趣味のPython・深層学習

中級者のための実装集

【2d reshape】スペクトル機械学習の魔法の考察

スペクトルデータの2次元化が機械学習モデルの性能を向上させる理由

入力データの次元性

機械学習モデル、特に畳み込みニューラルネットワーク(CNN)は、一般に2次元または3次元の入力データを扱うように設計されています。1次元のデータを直接CNNに入力すると、空間的な局所相関を捉えにくくなります。そのため、1次元のスペクトルデータを適切に高次元化する必要があります。

2次元化による利点

1次元のスペクトルデータを2次元に変換すると、以下の利点が得られます。

  1. 空間的局所相関の保持
    スペクトルデータには、隣接する波長間で相関がある可能性が高いです。2次元化によってこの局所相関が保持され、CNNがこの情報を効果的に学習できるようになります。

  2. チャネル次元の活用
    2次元テンソルにすることで、チャネル次元(3次元目の次元)を活用できます。この次元にスペクトルの異なる側面の情報(例えば位相)を割り当てることで、モデルが多角的な情報を利用できるようになります。

情報幾何学的観点

1次元のスペクトルデータは、ある高次元の滑らかな多様体(manifold)上の1次元の部分多様体に過ぎません。多様体とは、局所的にはユークリッド空間に様相構造を持つトポロジカルな空間のことです。スペクトルデータは本来、吸収係数や位相といった複数の変数で記述される高次元の構造を持っていると考えられます。

1次元の観測データからこの高次元多様体の構造を完全に復元するのは、測地的サンプリングの問題として知られており、非常に困難です。しかし、2次元化によってデータに多少の冗長性が生じ、この高次元多様体の接ベクトル束(tangent bundle)の局所的な構造が部分的に保たれます。

ベクトル束とは、多様体上の各点における接ベクトル空間の組み合わせで、多様体の局所的な線形構造を記述するものです。CNNは、この局所的な線形構造を効果的に抽出し、高い予測精度を実現したと考えられます。

数学的観点

1次元データを2次元へ変換することは、実質的に高次元ユークリッド空間への埋め込み(embedding)を行っていると解釈できます。この埋め込みは、シンプレクティック構造(symplectic structure)を保つ方法で行われます。

シンプレクティック構造とは、位相空間上で定義される非退化な閉ループ2次形式であり、力学系幾何学的構造を記述します。埋め込みによってこの構造が一部保たれることで、元のデータ空間の構造が損なわれずに次元が増やされます。

このように次元が増えると、モデルの表現力が飛躍的に向上します。モデルが表現可能な関数のなす空間(仮想的な再生核ヒルベルト空間)の次元が増え、より複雑な関数をよりうまく近似できるようになるためです。

さらに、2次元データに対する畳み込み演算は、離散コンボリューションの性質から計算効率が良くなります。これにより最適化が容易になり、性能向上につながります。

まとめ

以上のように、情報幾何学と数学の観点から、2次元変換がスペクトルデータの本質的な構造を保ちつつCNNに適した表現に変換することで、予測精度が大幅に改善されたと考えられます。

単純な前処理の工夫が、機械学習モデルの性能を大きく左右することを示す良い事例と言えるでしょう。データの構造を捉えるための次元変換は、機械学習における重要なテクニックの1つであると言えます。

PyTorch で複数モデルを繋げる方法

PyTorchで複数のモデルの出力をアンサンブルする方法

こんにちは。今日は、PyTorchで複数の深層学習モデルの出力をアンサンブルする方法について説明します。 アンサンブル学習は、単一のモデルよりも高い予測精度を得るための強力な手法です。異なる種類のモデルや同じモデルの異なる初期化からの複数の出力を組み合わせることで、モデルの一般化性能を向上させることができます。 今回は基礎編ということで非常にシンプルな例を解説します。

ここでは、2つの畳み込みニューラルネットワーク(CNN)の出力を結合し、全結合層に入力して損失を計算する例を示します。

※当然同じアーキテクチャでは多様性が獲得されませんが、今回は実装面の簡便さを重視します

ステップ1: ライブラリをインポートする

import torch
import torch.nn as nn
import torch.nn.functional as F

ステップ2: CNNモデルを定義する

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32 * 7 * 7, 64)
        self.fc2 = nn.Linear(64, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 32 * 7 * 7)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

この例では、2つの畳み込み層と2つの全結合層からなるCNNモデルを定義しています。入力は3チャンネルの画像で、出力は10クラスの確率分布です。

ステップ3: 2つのCNNモデルを作成する

model1 = CNN()
model2 = CNN()

ステップ4: 2つのモデルの出力を結合する

def ensemble_models(x):
    output1 = model1(x)
    output2 = model2(x)
    output = torch.cat((output1, output2), dim=1)
    return output

この関数では、2つのCNNモデルの出力を結合しています。torch.catを使用して、2つのテンソルを次元1(列方向)に沿って結合しています。つまり、2つの出力を単に連結しています。

ステップ5: 全結合層を定義する

fc = nn.Linear(20, 10)

ここでは、20次元の入力(2つのCNNモデルの出力の結合)を受け取り、10クラスに分類する全結合層を定義しています。

ステップ6: 損失関数と最適化アルゴリズムを定義する

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(list(model1.parameters()) + list(model2.parameters()) + list(fc.parameters()), lr=0.001)

交差エントロピー損失関数とSGD最適化アルゴリズムを定義しています。最適化アルゴリズムには、2つのCNNモデルと全結合層のパラメータをすべて含めています。

ステップ7: 学習とテストのループ

for epoch in range(num_epochs):
    for data, labels in train_loader:
        optimizer.zero_grad()
        outputs = fc(ensemble_models(data))
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    # テストデータでモデルを評価
    correct = 0
    total = 0
    with torch.no_grad():
        for data, labels in test_loader:
            outputs = fc(ensemble_models(data))
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    accuracy = 100 * correct / total
    print(f'Epoch [{epoch+1}/{num_epochs}], Accuracy: {accuracy:.2f}%')

学習ループでは、2つのCNNモデルの出力をensemble_models関数で結合し、全結合層に入力して損失を計算しています。その後、逆伝播と最適化を行います。 テストループでは、テストデータに対する予測を行い、正解率を計算しています。

残差ネットワークの本質

はじめに

近年、深層学習モデルの性能は目覚ましい進化を遂げてきました。しかしその一方で、ネットワークの層が極端に深くなるにつれ、勾配消失問題などの新たな課題が生じてきました。この問題を解決するため、2015年にMicrosoft ResearchのHe氏らによって残差ネットワーク(ResidualNet、ResNetと略される)が提案されました。

恒等写像の導入

ResNetの最大の特徴は、ネットワークに恒等写像(Identity mapping)を導入した点にあります。従来のネットワークでは、入力データに対して何らかの変換を行うことで特徴抽出を行っていました。しかし、ResNetでは入力データをそのまま出力に加算するショートカット結合(skip connection)が追加されています。

y = F(x, {Wi}) + x

上記の式で、xは入力、F(x, {Wi})は従来のネットワークによる変換、yは出力を表しています。ショートカット結合によって、x(入力データ)がyに加算されることで、恒等写像が実現されています。

残差の学習

ショートカット結合により、ネットワークは入力データxをベースにした上で、残差F(x, {Wi})-xを学習することになります。つまり、扱う情報量が減り、勾配消失問題のリスクが低下するのです。

また、このアーキテクチャにより、深層ネットワークの中に浅層ネットワークが自然と内包される形となります。初期段階では浅層ネットワーク(ショートカット結合)のみが機能し、徐々に残差F(x, {Wi})が最適化されていく、という具合です。

高い汎化性能

ResNetはILSVRC(ImageNet Large Scale Visual Recognition Challenge)などの画像認識タスクで高い性能を発揮し、深層学習の新しい地平を切り開きました。恒等写像の導入により、層が深くなるほど性能が向上する良好な挙動を示しています。

ResNetの登場以降、多くのネットワークがこの設計思想を取り入れ、Computer Visionを中心に幅広い分野で応用が進んでいます。残差ネットワークは、深層学習モデルの新しい構築法を我々に提示した、極めて重要な技術innovation(革新)であると言えるでしょう。

pandas Dataframeの重み付きアンサンブル

重み付きアンサンブルとは?

重み付きアンサンブルは、複数のモデルの予測結果に対して異なる重みを割り当て、それらを組み合わせる手法です。各モデルに与えられる重みは、そのモデルの性能や信頼性に基づいて決定されます。この方法は、アンサンブル全体の性能を向上させるのに寄与します。

サンプルコード

ここでは、kaggleでnotebookを提出するときに便利なデータフレームでのアンサンブルサンプルコードを提供します。

import pandas as pd

def calculate_weighted_average(*dfs, weights=None):
    # dfsが空でないことを確認
    if not dfs:
        raise ValueError("No DataFrames provided.")
    
    # カラムリストを取得
    columns = dfs[0].columns
    
    # 重みが提供されているか確認
    if weights is None:
        weights = [1] * len(dfs)
    elif len(weights) != len(dfs):
        raise ValueError("Number of weights must match the number of DataFrames.")
    
    # それぞれのカラムごとに重みつき平均を計算
    weighted_avg_df = pd.DataFrame({'ID': dfs[0]['ID']})
    
    for column in columns:
        weighted_avg_df[column] = sum(df[column] * weight for df, weight in zip(dfs, weights)) / sum(weights)
    
    return weighted_avg_df

# 三つのダミーデータを作成
df1 = pd.DataFrame({'ID': [1, 2, 3],
                    'seizure_vote': [10, 20, 30],
                    'lpd_vote': [15, 25, 35],
                    'gpd_vote': [12, 22, 32],
                    'lrda_vote': [18, 28, 38],
                    'grda_vote': [14, 24, 34],
                    'other_vote': [16, 26, 36]})

df2 = pd.DataFrame({'ID': [1, 2, 3],
                    'seizure_vote': [12, 22, 32],
                    'lpd_vote': [16, 26, 36],
                    'gpd_vote': [14, 24, 34],
                    'lrda_vote': [20, 30, 40],
                    'grda_vote': [18, 28, 38],
                    'other_vote': [22, 32, 42]})

df3 = pd.DataFrame({'ID': [1, 2, 3],
                    'seizure_vote': [14, 24, 34],
                    'lpd_vote': [18, 28, 38],
                    'gpd_vote': [16, 26, 36],
                    'lrda_vote': [22, 32, 42],
                    'grda_vote': [20, 30, 40],
                    'other_vote': [24, 34, 44]})

# 重みのリストを作成
weights = [0.3, 0.5, 0.2]

# 関数を呼び出して重み付き平均を計算
result_df = calculate_weighted_average(df1, df2, df3, weights=weights)

# 結果を表示
print(result_df)

nn.ModuleListを解説してみる

PyTorchのnn.ModuleListとは?

nn.ModuleList は、PyTorchのニューラルネットワークモジュールの一部であり、複数の nn.Module オブジェクトをまとめて保持するためのコンテナです。これを使うことで、モデル内で複数のサブモデルを簡潔に管理できます。

なぜnn.ModuleListを使用するのか?

柔軟性と再利用性: nn.ModuleList を使うと、動的なサブモデルの追加や取り外しが可能になります。これにより、モデルを構築する際により柔軟で再利用可能なコードを書くことができます。 パラメータ管理: nn.ModuleList は、リスト内の各モジュールが持つパラメータを自動的にトラッキングします。これにより、モデル全体のパラメータ管理が簡単になります。

例: nn.ModuleListの使用

以下は、nn.ModuleList を使用していくつかのサブモデルを保持する例です。

import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()

        # nn.ModuleListでサブモデルを保持
        self.submodules = nn.ModuleList([nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 5)])

    def forward(self, x):
        # サブモデルの順伝播
        for layer in self.submodules:
            x = layer(x)
        return x

この例では、nn.ModuleList には nn.Linear と nn.ReLU の層が含まれています。これにより、モデル内でこれらの層を管理し、効果的に利用することができます。

下記ではSelf attentionのクラスを実装する例です

# 自己注意機構(Self-Attention)のヘッドを複数組み合わせるためのクラス
class SelfAttention_MultiHeads(nn.Module):

    # 初期化メソッド
    def __init__(self, n_mbed, num_heads, head_size, block_size):
        super().__init__()

        # Self-Attentionのヘッドをリストで保持するModuleListを作成
        self.heads = nn.ModuleList((SelfAttention_Head(n_mbed, head_size, block_size) for _ in range(num_heads)))

    # 順伝播メソッド
    def forward(self, x):
        # 各ヘッドに入力を渡し、結果を横方向(最後の次元)に結合して返す
        return torch.cat([h(x) for h in self.heads], dim=-1)

内包表記と組み合わせています。 内包表記を使用しない例を参考にあげておきます。

class SelfAttention_MultiHeads(nn.Module):

    # 初期化メソッド
    def __init__(self, n_mbed, num_heads, head_size, block_size):
        super().__init__()

        # Self-Attentionのヘッドをリストで保持するModuleListを作成
        self.heads = nn.ModuleList()
        for _ in range(num_heads):
            self.heads.append(SelfAttention_Head(n_mbed, head_size, block_size))

    # 順伝播メソッド
    def forward(self, x):
        # 各ヘッドに入力を渡し、結果を横方向(最後の次元)に結合して返す
        results = []
        for head in self.heads:
            results.append(head(x))
        return torch.cat(results, dim=-1)

1分で見るPythonicなコード例

本ブログでは、Pythonコードをより簡潔で読みやすく、かつ効果的に書くための3つのPythonicなテクニックを紹介します。これらの方法はリスト内包表記、条件式の値の交換、そして辞書内包表記です。

1. リスト内包表記を使用して平坦化する

# リスト内包表記を使用して平坦化
flat_list = [item for sublist in nested_list for item in sublist]

上記のコードは、ネストされたリストを平坦化するためのシンプルな手法です。nested_list がネストされたリストを含む場合、flat_list はそれを平坦化したリストになります。この方法はコードを簡潔にし、可読性を向上させます。

2. 条件式を使用して値を交換する

# 条件式を使用して値を交換
a, b = 5, 10
a, b = b, a if a > b else a, b

上記の例では、a と b の値を交換する際に条件式を利用しています。これにより、1行でシンプルに値を交換することができます。可読性が向上し、コードの行数も削減されます。

3. 辞書内包表記を使用してリストの要素を指定の条件でフィルタリング

# 辞書内包表記を使用してリストの要素をフィルタリング
original_dict = {'a': 1, 'b': 2, 'c': 3}
filtered_dict = {k: v for k, v in original_dict.items() if v > 1}

上記のコードでは、辞書内包表記を使用して特定の条件でリストの要素をフィルタリングしています。original_dict から値が1未満の要素をフィルタリングして、filtered_dict を作成しています。

これらのPythonicな書き方は、コードを効率的かつ美しく保つ方法です。ただし、美しいコードは主観的な側面も強いですので、個々の好みやチームのスタイルにも影響されることを考慮してください。どのスタイルが最も適しているかは、プロジェクトや状況によって異なるかもしれません。

辞書のアンパック

Pythonでは、関数に引数を渡す方法が様々ありますが、その中でも辞書をアンパックして引数として渡すことは便利なテクニックです。この記事では、辞書のアンパックを使った引数の渡し方について詳しく解説します。

辞書のアンパックとは?

辞書のアンパックは、辞書のキーと値を分解して、それを関数の引数として渡す方法です。これにより、可変数のキーワード引数を使った柔軟な関数の定義が可能になります。

基本的なアンパックの使い方

def example_function(name, age, city):
    print(f"Name: {name}, Age: {age}, City: {city}")

# 辞書を定義
user_info = {"name": "John", "age": 25, "city": "Tokyo"}

# 辞書をアンパックして関数に渡す
example_function(**user_info)

この例では、example_functionに対して辞書user_infoをアンパックして引数として渡しています。

デフォルト値との組み合わせ アンパックを使うと、デフォルト値を持つ引数と組み合わせることができます。

def example_function(name, age=30, city="Unknown"):
    print(f"Name: {name}, Age: {age}, City: {city}")

# 辞書を定義(ageのみ指定)
user_info = {"name": "Alice"}

# 辞書をアンパックして関数に渡す
example_function(**user_info)

この例では、デフォルト値を持つ引数と辞書のアンパックを組み合わせています。