ayujaja
@ayujaja

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Pythonでニューラルネットワーク

3D座標データの集まりを1つの軌跡としてテキストファイルに保存しています。そのデータをニューラルネットワークで学習させ学習モデルを作り、新しい軌跡データが入力された時にどの軌跡に分類されるか判断するプログラムを作成しようとしています。しかし、作成したプログラムを実行すると全然正解してくれません。解決方法を教えてください。

例)
Ruby on RailsでQiitaのようなWebアプリをつくっています。
記事を投稿する機能の実装中にエラーが発生しました。
解決方法を教えて下さい。

発生している問題・エラー

エラーは発生しないのですが、どんなデータを入れても予測クラスの数値がラベル順に大きくなります。

例)
ラベル1の予測クラス: 79
ラベル2の予測クラス: 119
ラベル3の予測クラス: 159
ラベル4の予測クラス: 199

該当するソースコード

ソースコード
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import os

# ニューラルネットワークのモデルを定義
class FixedNet(nn.Module):
    def __init__(self, input_size, output_size):
        super(FixedNet, self).__init__()
        self.fc1 = nn.Linear(input_size, 64)
        self.fc2 = nn.Linear(64, 32)
        self.fc3 = nn.Linear(32, output_size)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# ファイル名からラベル情報を抽出する関数
def extract_label_from_filename(filename):
    parts = filename.split("_")
    if len(parts) >= 2:
        label_part = parts[1].replace("label", "")
        return int(label_part)
    else:
        return None

# ラベルを整数からファイル名の形式に変換する関数
def label_to_filename(label):
    return f"label{label}_model.pth"

# データファイルのメインディレクトリ
main_data_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata"

# 学習済みモデルの保存ディレクトリ
model_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク2"

# ニューラルネットワークモデルの初期化
input_size = 3  # 3D座標データの次元

# 損失関数と最適化アルゴリズムを定義
criterion = nn.CrossEntropyLoss()

# 学習のループ
num_epochs = 10

# ラベルごとにデータセットとモデルを管理するための辞書を作成
label_datasets = {}
label_models = {}

# 各ディレクトリ内のファイルを順番に処理
for label_directory in os.listdir(main_data_directory):
    label = label_directory  # ラベルはディレクトリ名と同じと仮定

    # ラベルごとのデータディレクトリ
    data_directory = os.path.join(main_data_directory, label_directory)

    # 学習データを格納するリスト
    data = []
    labels = []

    # データディレクトリ内のファイルを処理
    for filename in os.listdir(data_directory):
        # ファイル名からラベルを抽出
        label = extract_label_from_filename(filename)
        if label:
            # データファイルのパス
            file_path = os.path.join(data_directory, filename)

            # データの読み込みと前処理
            with open(file_path, 'r') as file:
                for line in file:
                    parts = line.strip().split()
                    data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
                    data.append(data_point)
                    labels.append(int(label))  # ラベルを整数に変換

    # データをPyTorchのテンソルに変換
    data = torch.tensor(data, dtype=torch.float32)
    labels = torch.tensor(labels, dtype=torch.long)

    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

    # ニューラルネットワークモデルの初期化
    output_size = max(labels).item() + 1
    model = FixedNet(input_size, output_size)

    # 各ラベルごとにデータセットとモデルを保存
    label_datasets[label] = data_loader
    label_models[label] = model

    # ラベルごとにモデルを訓練
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    for epoch in range(num_epochs):
        for data_batch, labels_batch in data_loader:
            optimizer.zero_grad()  # 勾配を初期化
            outputs = model(data_batch)  # モデルの予測
            loss = criterion(outputs, labels_batch)  # 損失を計算
            loss.backward()  # 逆伝播
            optimizer.step()  # パラメータの更新

    # 学習済みモデルを保存
    os.makedirs(model_directory, exist_ok=True)
    model_filename = label_to_filename(label)  # モデルのファイル名を生成
    model_save_path = os.path.join(model_directory, model_filename)

    # ラベルの整数値を保存
    with open(os.path.join(model_directory, f"label{label}.txt"), 'w') as label_file:
        label_file.write(str(label))

    torch.save(model.state_dict(), model_save_path)

# 新しいデータを予測
new_data_file = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata/label1/coordinates_label1_20231016194432.txt"
new_data = []

with open(new_data_file, 'r') as file:
    for line in file:
        parts = line.strip().split()
        data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
        new_data.append(data_point)

# 新しいデータをPyTorchのテンソルに変換
new_data = torch.tensor(new_data, dtype=torch.float32)

# 各学習済みモデルを使って新しいデータを予測
predictions = []

for label in os.listdir(model_directory):
    if label.endswith(".pth"):
        # ラベルの整数値を取得
        label_int = int(label.split("_")[0].replace("label", ""))
        model_filename = label_to_filename(label_int)
        model_path = os.path.join(model_directory, label)

        # 学習済みモデルのロード前にデバッグ情報を表示
        print(f"ロード対象のモデルファイルパス: {model_path}")

        # 学習済みモデルをロード
        if os.path.exists(model_path):
            # 予測モデルの形状に合わせて新しいモデルを初期化
            model = label_models.get(label_int)
            if model is not None:
                model.load_state_dict(torch.load(model_path))
                model.eval()
                with torch.no_grad():
                    output = model(new_data)
                    predictions.append((label_int, output))
            else:
                print(f"ラベル {label_int} のモデルが見つかりません")
        else:
            print(f"モデルが見つかりません: {model_path}")

# 予測結果の確認
sorted_predictions = sorted(predictions, key=lambda x: x[0])  # ラベルの数値順にソート
for label_int, output in sorted_predictions:
    predicted_class = torch.argmax(output).item()
    print(f"ラベル{label_int}の予測クラス: {predicted_class}")

###このプログラムの詳細
このプログラムは、ニューラルネットワークモデルをトレーニングし、新しいデータのラベルを予測するためのものです。下記にその手順を示します。

 ニューラルネットワークのモデルを定義します。FixedNet クラスは、3次元の入力を受け取り、線形レイヤーを組み合わせてニューラルネットワークを構築します。

 ファイル名からラベル情報を抽出するための関数 extract_label_from_filename を定義します。これは、ファイル名からラベルを抽出するユーティリティ関数です。

 ラベルを整数からファイル名の形式に変換する関数 label_to_filename を定義します。これは、ラベルをモデルのファイル名に変換するためのユーティリティ関数です。

 メインデータディレクトリと学習済みモデルの保存ディレクトリを指定します。ここで、学習データと学習済みモデルを格納するディレクトリのパスを指定します。

 ニューラルネットワークモデルの初期化に必要な入力次元数(input_size)を定義します。

 損失関数(criterion)をクロスエントロピー損失関数で、最適化アルゴリズムを Adam で初期化します。

 ラベルごとにデータセットとモデルを管理するための辞書を作成します。

 メインデータディレクトリ内の各ラベルディレクトリをループで処理し、学習データを収集し、対応するモデルを訓練します。学習データはファイルから読み込まれ、PyTorchのテンソルに変換され、DataLoader を使用してミニバッチとしてロードされます。

 各ラベルごとにモデルを訓練し、学習済みモデルを保存します。モデルは指定されたディレクトリに保存され、ファイル名はラベルに関連づけられます。

 新しいデータを読み込み、各学習済みモデルを使用して新しいデータのラベルを予測します。各学習済みモデルは保存されたファイルからロードされ、新しいデータに適用されます。

 最後に、予測されたラベルとそのクラスを表示します。ラベルは label{label}_model.pth のファイル名形式と一致するように整数から変換され、予測クラスが表示されます。

このプログラムで、なにか間違いがあれば教えてください。

0

1Answer

おそらくデータセットの構築がやりたいことと一致していないのが原因と思われます.

ラベルごとにモデルを作ってますがこれの意図を伺いたいです.
少なくとも一般的なデータセットの構築方法とは大きく異なります.

1Like

Comments

  1. @ayujaja

    Questioner

    回答ありがとうございます。
    ラベルごとに学習モデルを作っている意図としては、ラベルごとにモデルを作ることで、各ラベルの特性をキャプチャしやすくなると考えたからです。それによって、全体で一つのモデルを作るよりも高精度な判断ができると考え、各ラベルごとに学習モデルを作成し、比較するプログラムとしました。
    やはり、データ全体を学習させ、学習モデルは1つにまとめた方が良いのでしょうか?何かご存知であれば、聞かせて頂きたいです。
    また、他にも不明な点があればなんでも聞いてください。

  2. ラベルごとにモデルを作ることで、各ラベルの特性をキャプチャしやすくなると考えた

    非常に良い試みだと思いますが,残念なことに機械学習では比較対象がないと正しく学習ができないのです.1ラベルだけを与えて学習させても,モデルがlazyになりどんな入力が来てもそのラベルを出せばいいのだと学習されるだけです.

    データ全体を学習させ、学習モデルは1つにまとめた方が良い

    この文言から考えるに,初手で質問のモデルに取り組もうとされているのでしょうか.普通は一般的なモデルを採用して精度のベースラインを決め,それを上回るようにモデルを改良していくものです.

    ベースラインとの比較のために,モデルの出力は同じである必要がありますね.今回,ラベルが大量に出来てしまったということは,何を出力したらよいか不明瞭なまま進めたように見えてしまいます.


    ちなみにどこで聞いたか忘れましたが,特定のラベルを省いたモデルを学習させたのちに,省いていたラベルを1つ1つ追加していってロバストなモデルを作る試みを見たことがあります.事前学習や転移学習の範疇ですがこういう試みならアリだと思います(ちゃんと比較対象があるので).


    次に伺いたいことですが,

    3D座標データの集まりを1つの軌跡

    とのことで,データのあるファイル名を見るからにOpenPoseを利用して得られた時系列点群を処理したいように見えます.

    よくありそうな研究で言えばバレーのサーブの動作のうち右手の動きを3Dで抽出してきて,素人かプロか判断する.みたいなのがあったときを考えてみます.

    このとき,ある一瞬の右手の座標だけから判断ができるものではなく,時系列として動きを持ってないといけませんよね.

    つまり,モデルに与えられるべきデータは,動画で言うところのフレーム数×3次元の量のデータを入力として取って,素人かプロかを示すラベルを出力することになるはずです.

    しかし,質問のモデルはinput_size = 3であり,まさかの「ある一瞬の右手の座標だけから判断」しようとするモデルになっています.「3D座標データの集まりを1つの軌跡として」扱いたいのに対して,単一3D座標のみを入力として取っている現状では,データセット及びモデルの構築が誤っていると考えますが,この点に関してはいかがでしょうか.

    データセットの構成から見直すためにまずはmain_data_directory配下のフォルダやファイルの状態をtreeを使って記述いただけると嬉しいです.treeコマンドが入っている環境なら,main_data_directoryで実行した結果をコードブロックに貼り付けてください.

  3. @ayujaja

    Questioner

    回答ありがとうございます。
    各ラベルごとに学習してしまうと、そのような学習モデルとなってしまうのですね、非常に参考になりました。

    ————————————————————————
    初手で質問のモデルに取り組もうとしているか、というのはどういうことでしょうか?
    私は、機械学習に取り組むのが初めてでして、恥ずかしながらあまり知識がない状態なのですが、私の作成したモデルは一般的なモデルではないのでしょうか?
    初めにも書きましたが、軌跡データを学習することで、新しい軌跡を分類できるような学習済みモデルを作成したいと考えております。

    ———————————————————————

    最後に、軌跡データとして認識してないのでは?という質問に関してです。

    おっしゃる通り、OpenPoseを用いて3D座標データを取得して、学習用ファイルを作っています。最初に記載したプログラムでは、座標データを1つの軌跡とするプログラムは含まれていません。しかし、OpenPoseでデータを取る際に、取得したデータポイントを随時リストに追加していき、プログラム終了と共に、リストの中の座標データを1つにまとめて軌跡データとしラベル付けするようにしております。これによって、保存されたファイルは軌跡データとして扱われ、学習するために読み込む際も、座標データを軌跡にする前処理などは必要ないと考えたのですが、どうでしょうか?何かご意見あれば、詳しく聞かせて頂きたいです。

    最後に、実行結果を貼り付けて、参考にして頂きたいところなのですが、本日はその環境が揃っていないため明日以降になってしまいます。参考にするものが少なくて申し訳ございません。
    プログラムの詳細は明日以降なら添付できます。

  4. 作成したモデルは一般的なモデルではないのでしょうか

    すみません,これは説明不足でした.

    質問で作成されているFixedNetは,非常に一般的なモデルでチュートリアルでもよく見かけるモデルになっています.同時にチュートリアルでよく見るirisデータセットに対して分類する際に,irisのパラメータである花びらの特徴4つを入力として受け取り,3種類のラベルを出力するようなモデルでは(過学習しそうではあるものの)非常に有効です.

    つまり,input_size特徴量を入力として受け取り,output_size個のラベルを出力する一般的なモデルですね.

    しかし,軌跡という時系列的前後関係のあるデータ形式を入力として取るモデルではなく,ある3D座標たった1点を入力としてとり,ラベルを出力するモデルになっています.これは,「時系列解析モデルとして」一般的なモデルでは無い.という話をしたかったです.

    現状のFixedNet単体でできる分類作業は,3D座標のどの座標がどのラベルに振り分けられるか.というタスクになります.これはやりたいことと一致しませんね.

    OpenPoseでデータを取る際に、取得したデータポイントを随時リストに追加していき、プログラム終了と共に、リストの中の座標データを1つにまとめて軌跡データとしラベル付けするようにしております。これによって、保存されたファイルは軌跡データとして扱われ、

    なるほど,であれば,1ファイル1軌跡ということでしょうか.1ファイルの中に複数の軌跡が入っているのでしょうか.おそらく前者だと思いますが一旦そうだとして話を進めると

    学習するために読み込む際も、座標データを軌跡にする前処理などは必要ないと考えた

    ここに誤りがありそうです.1ファイル1軌跡だというのに,1ファイルからその行数分の3D座標データを抽出し,それぞれにラベルを振っているというのが現状です.主に次のコードの箇所ですね.

    # データの読み込みと前処理
    with open(file_path, 'r') as file:   # ファイルを読み込み
        for line in file:                # ファイルの各行に対して
            parts = line.strip().split() # 空白区切りのデータをsplitし
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])] # 0~2番目のデータを3D座標のxyzとする
            data.append(data_point)      # 入力するデータとして追加
            labels.append(int(label))    # ラベルを整数に変換して出力データとして追加
    

    ここに軌跡として処理している箇所は無いですね.

    1ファイル1軌跡だとしたときには,次のようになります.

    # データの読み込みと前処理
    trajectory = list() # データポイントをまとめ,軌跡データとするためのリスト
    with open(file_path, 'r') as file:   # ファイルを読み込み
        for line in file:                # ファイルの各行に対して
            parts = line.strip().split() # 空白区切りのデータをsplitし
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])] # 0~2番目のデータを3D座標のxyzとする
            trajectory.append(data_point) # 軌跡データに追加
    data.append(trajectory)       # 軌跡データを入力データとして追加
    labels.append(int(label))    # ラベルを整数に変換して出力データとして追加
    

    これはまさに

    OpenPoseでデータを取る際に、取得したデータポイントを随時リストに追加していき、プログラム終了と共に、リストの中の座標データを1つにまとめて軌跡データとしラベル付けする

    と対応していて,「Pythonでデータを読み出す際に,読み出したデータポイントを随時リストに追加していき,読み出し終了と共に,リストの中の座標データを1つにまとめて軌跡データとしてラベル付けする」ことになります.

    これで,モデルに入力するデータの形状がファイルの行数x3という形1になり,時系列解析に取り組む際の入力データの形式を得ました.そして,これは質問のモデルFixedNetの形状に一致しませんのであとで直す必要があります.

    さてその前に,次の疑問になるのが「ファイルの行数はすべて一致しているのか」という点ですね.モデルに与えるデータ形状はすべてのデータにおいて一緒でなくてはなりませんから,行数不一致であればデータを揃えるための更なる前処理を要します.

    初手で質問のモデルに取り組もうとしているか、というのはどういうことでしょうか?

    普通は「一般的なモデルを採用して精度のベースラインを決め,それを上回るようにモデルを改良していくもの」であるからして,「ラベルごとにモデルを作ることで、各ラベルの特性をキャプチャしやすくなると考え」ることは,最初にやることでは無いということです.

    1. i番目に読み込んだファイルに対してtorch.Tensor(data[i]).size()(ファイルの行数, 3)になるはずです.

  5. @ayujaja

    Questioner

    すぐに回答していただいてありがたい限りです。

    説明の詳細ありがとうございます。「時系列解析モデルとして」一般的なモデルでは無い.という話は完全に理解しました。確かに、3D座標データ一つ一つがどのラベルに属するかを判断するのは、私のやりたい事ではありません。

    ——————————————————
    1つ目の質問に関してですが、おっしゃる通り1ファイル1軌跡として、ラベルごとにディレクトリを分けて保存しているのが現状です。

    モデルに入力するデータの形状がファイルの行数x3という形1になり,時系列解析に取り組む際の入力データの形式を得ました.そして,これは質問のモデルFixedNetの形状に一致しませんのであとで直す必要があります

    ご指摘していただいた修正箇所は、確かにモデルの3D座標1つ1つにラベルを振り分けてることになり、それによって求めたい出力結果が得られないということが理解できました。
    また、モデルの形状を直す必要がある、ということも理解しました。

    ——————————————————

    次の質問に関してですが、ファイルの行数はファイルによってそれぞれ異なります。「行数を揃える前処理を行なってから学習モデルを作っていく必要がある」ということで間違い無いでしょうか?

    ——————————————————

    最後に、「普通は「一般的なモデルを採用して精度のベースラインを決め,それを上回るようにモデルを改良していくもの」であるからして,「ラベルごとにモデルを作ることで、各ラベルの特性をキャプチャしやすくなると考え」ることは,最初にやることでは無いということ」についてですが、これに関しても理解できました。詳細な説明をして頂きありがとうございます。

  6. 理解が早くて助かります.

    「行数を揃える前処理を行なってから学習モデルを作っていく必要がある」ということで間違い無いでしょうか?

    はい.画像処理での前処理で例えると,「画像サイズの縦横比(というより縦横のピクセル数)が画像ごとに違う」というケースが多々あり,処理のためにいくつかの選択肢があります.

    • 選択肢1: 縦横比を無理やり変えて任意の縦横ピクセル数に揃える
    • 選択肢2: 縦横比が揃うように画像の一部を切り出す

    縦横比が変わるのは少々問題があり,画像に限らずOpenPoseで得られる軌跡データでも一緒です.1秒当たりに取得した3D座標の数は共通のはずですから,これを縮小/拡大する処理を実行してしまうと全く同じ動きをしていてもファイルごとに3D座標の移動速度が変わることになってしまいます.

    画像処理では,これを避けるために,

    • 選択肢2.1: 画像の中心から揃えたいピクセル数の分だけを縦横で取ってきて同じサイズの画像を得る
    • 選択肢2.2: 1epochごとに切り取る箇所を変えてonline data augmentationに繋げる1

    ということをします.これら複数の手段がある中で,質問者はOpenPoseで得られたデータを眺めて,どれを選ぶか自分の心と相談することになります.

    まずは,ファイルの行数の統計をとるところからになりそうです.全体で見てほんの数行しか差分がないなら,ファイルの先頭と末尾を削除して行数最小値に合わせるだけでよいですし,行数に差分が大きく,どこで切りとっても識別可能そうなら選択肢2.2を選ぶとonline data augmentationに繋がります.

    また,手作業ですべてのファイルの行数を揃えたデータセットを作っても構いません.

    1. 画像前処理用のPyTorchであるtorchvisionにはRandomCropが用意されており,これをDataLoaderでのデータ呼び出し時に適用するだけで実現可能です.残念ながら時系列データ用の前処理ライブラリがそもそもPyTorchに無いです.

  7. @ayujaja

    Questioner

    何度も質問に答えて頂きありがとうございます。

    軌跡データを前処理で行数を揃える方法としましては、取得する3D座標点の数にそこまでの差はないので、座標データを行数最小値に合わせる2.1の方法でやってみようと思います。

    ——————————————————

    最後に、これまでご指摘していただいた修正点をまとめてみます。

    ・ 学習モデルを、ラベルごとではなく、全てのデータに対して学習し1つのモデルを作成するように変更する。

    ・データを読み込む際に3D座標データ1つ1つにラベル付けをしてしまっていることから、軌跡データとしてラベル付けするように直す。

    ・入力するデータの形状は軌跡データなので、「ファイルの行数x3」という時系列解析に取り組む際の入力データの形式であり、モデルFixedNetの形状をそれに合わせる。

    ・学習するファイルの行数(データの数)を合わせる前処理を追加する。

    これらの修正点が間違っている場合や、他にも修正点があれば是非教えて頂きたいです。
    今のところ、この修正で大丈夫そうでしたら、これらの修正を行い、再度挑戦してみます!

  8. 入力するデータの形状は軌跡データなので、「ファイルの行数x3」という時系列解析に取り組む際の入力データの形式であり、モデルFixedNetの形状をそれに合わせる。

    ここだけ注意させてください.

    ただのNeural NetworkであるFixedNetを利用する場合は入力の形状をファイルの行数x3にした上で,FixedNet入力直後のxtorch.nn.Flattenを差し込めばひとまず解析可能です.性能が良すぎて過学習する可能性がありますが,これをベースラインにしても良いと思います.汎化性能の改善を目的として帰納バイアスのあるRecurrent Neural Network系を利用することになると思います.

    現状ただのNNであることに対して,時系列解析を目的としたRNNを採用したり,それの応用であるLSTMやGRUを採用したり,果てはChatGPTのモデルでも使われるTransformerなど使うことが可能です1

    1. これらの時系列解析用のモデルを使ったカテゴリ分類では大抵の場合,別のタイミングでtorch.nn.Flattenを差し込むことになります.x.view()みたいな記述を見かけるかもしれません.今後データの形状操作が頻繁に出てくるはずなので頭の隅にでも入れておいてください.

  9. 言い忘れてましたが,入力データの正規化も忘れないようにしてください.NNは0近傍の入力値を得意としますが,あまりにも0から大きく離れた値は苦手です.

    正規化の範囲ごとに幾つかの種類があります.

    • Dataset-wise Normalization: データセット全体で正規化
      • データセット全体で値域や変動域が類似している場合に有効かも.
      • 質問のコードでいうところのdataに対して正規化
    • Sample-wise Normalization: 1ファイルごとに正規化
      • 各軸の変動量が同じぐらいのときに有効.それぞれの軸が平等に扱われるためOpenPose向きかも
      • trajectoryを使ったコードでいうところのdata[i]に対して正規化
    • Channel-wise Normalization: 3D座標のxyzそれぞれの軸で正規化.
      • たとえば横移動をメインとしたデータを解析したい場合1軸のみの変動量が大きいことになるので,有効かもしれない.ただし横移動の大小で判別することになるのであれば,正規化で潰れてしまうので注意(Sample-wiseであれば他の軸との相対評価で横移動の大小を識別可能なはず).
      • trajectoryを使ったコードでいうところのdata[i][:, 0],data[i][:, 1],data[i][:, 2]がxyz各軸を意味するので,それぞれで正規化

    その上で,次の正規化手法を行うことになります.

    • Min-Max Normalization: 最小値と最大値を任意の値域に収める
      • 値域$[0, 1]$: 最小値付近の効果を小さくしたい場合に有効.
      • 値域$[-1, 1]$: 最小値付近の値にも意味がある場合に有効.画像処理でよく見る1
      • 外れ値の存在は,他のデータポイントを狭い範囲に圧縮する可能性があり,データの解釈を困難にする可能性がある.
    • Z-score Normalization: 分散1,平均0に収める2
      • Neural Networkの得意とする値域に納めつつ,さらにどのようなデータでも0中心にセンタリングを行う効果が得られる.
      • OpenPoseでもし画面端っこでの動作をキャプチャしてしまっていても,Channel-wise Z-score Normalizationで実行すればどのファイルも3D点群の重心位置を揃えて(画面真ん中で動いているように)処理できる.
      • 外れ値は平均と標準偏差を大きく影響させる可能性があり,結果として正規化されたデータセットの分布が歪む可能性がある.
    1. RGBの値のどれかが小さい,ということはたとえばGreenが無い色はRed+Blueで紫系統色になるという意味になる.

    2. Standardization,標準化とも呼ばれる.

  10. @ayujaja

    Questioner

    何度も質問申し訳ありません。

    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.utils.data import DataLoader, TensorDataset
    import os
    
    # ニューラルネットワークのモデルを定義 (LSTMを使用)
    class FixedNet(nn.Module):
        def __init__(self, input_size, hidden_size, num_classes):
            super(FixedNet, self).__init__()
            self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
            self.fc = nn.Linear(hidden_size, num_classes)
    
        def forward(self, x):
            out, _ = self.lstm(x)
            out = self.fc(out[:, -1, :])  # 最後の時刻の出力を使う
            return out
    
    # ディレクトリ名からラベル情報を抽出する関数
    def extract_label_from_directory(directory_name):
        # ディレクトリ名から "label" 接頭辞を削除して整数ラベルを取得
        label = int(directory_name.replace("label", "")) - 1  # ラベルを 0 から始めるように変更
        return label
    
    
    # データファイルのメインディレクトリ
    main_data_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata"
    
    # 学習済みモデルの保存ディレクトリ
    model_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク2"
    
    # ニューラルネットワークモデルの初期化
    input_size = 3  # 3D座標データの次元
    hidden_size = 64  # LSTMの隠れ層のサイズ
    output_size = 4  # ラベル数( ラベル0, ラベル1, ラベル2, ラベル3)
    
    # 損失関数と最適化アルゴリズムを定義
    criterion = nn.CrossEntropyLoss()
    
    # 学習のループ
    num_epochs = 10
    
    # 学習データを格納するリスト
    data = []
    labels = []
    
    # 各ディレクトリ内のファイルを順番に処理
    for label_directory in os.listdir(main_data_directory):
        label = extract_label_from_directory(label_directory)  # ラベルをディレクトリ名から抽出
    
        # ラベルごとのデータディレクトリ
        data_directory = os.path.join(main_data_directory, label_directory)
    
        # データディレクトリ内のファイルを処理
        for filename in os.listdir(data_directory):
            # ファイル名からラベルを抽出
            label = extract_label_from_directory(label_directory)
            if label:
                # データファイルのパス
                file_path = os.path.join(data_directory, filename)
    
                # データの読み込みと前処理
                trajectory = list()  # データポイントをまとめ,軌跡データとするためのリスト
                with open(file_path, 'r') as file:  # ファイルを読み込み
                    for line in file:  # ファイルの各行に対して
                        parts = line.strip().split()  # 空白区切りのデータをsplitし
                        data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
                        trajectory.append(data_point)  # 軌跡データに追加
                data.append(trajectory)  # 軌跡データをデータリストに追加
                labels.append(label)  # ラベルを整数に変換してラベルリストに追加
    
    
    # 最小の軌跡データ長に合わせる前処理
    min_length = min(len(traj) for traj in data)
    data = [traj[:min_length] for traj in data]
    
    # データとラベルをテンソルに変換
    data = torch.tensor(data, dtype=torch.float32)
    labels = torch.tensor(labels, dtype=torch.long)
    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    
    # ニューラルネットワークモデルの初期化
    model = FixedNet(input_size, hidden_size, output_size)
    
    #トレーニング
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    for epoch in range(num_epochs):
        for data_batch, labels_batch in data_loader:
            optimizer.zero_grad()  # 勾配を初期化
            print(data_batch.shape)  # データバッチの形状を確認
            outputs = model(data_batch.permute(0, 2, 1))  # モデルの予測
            loss = criterion(outputs, labels_batch)  # クロスエントロピー損失を計算
            loss.backward()  # 逆伝播
            optimizer.step()  # パラメータの更新
    
    # 学習済みモデルを保存
    os.makedirs(model_directory, exist_ok=True)
    model_filename = "combined_model.pth"
    model_save_path = os.path.join(model_directory, model_filename)
    
    torch.save(model.state_dict(), model_save_path)
    
    # 新しいデータを予測
    new_data_file = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata/label3/coordinates_label3_20231016194808.txt"
    new_data = []
    
    with open(new_data_file, 'r') as file:
        for line in file:
            parts = line.strip().split()
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
            new_data.append(data_point)
    
    # 新しいデータのシーケンス長を最小の軌跡データ長に合わせる
    new_data = new_data[:min_length]
    
    # 新しいデータをPyTorchのテンソルに変換
    new_data = torch.tensor(new_data, dtype=torch.float32).view(1, -1, input_size)  # バッチサイズ x シーケンス長 x 入力次元
    
    # 学習済みモデルをロード
    model.load_state_dict(torch.load(model_save_path))
    model.eval()
    with torch.no_grad():
        output = model(new_data.permute(0, 2, 1))
        class_probabilities = torch.softmax(output, dim=2)
        predicted_class = torch.argmax(class_probabilities, dim=2).item()
        print(f"新しいデータの予測クラス: {predicted_class}")
    
    
    # データファイルのメインディレクトリ
    main_data_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata"
    
    # 学習済みモデルの保存ディレクトリ
    model_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク2"
    
    # ニューラルネットワークモデルの初期化
    input_size = 3  # 3D座標データの次元
    hidden_size = 64  # LSTMの隠れ層のサイズ
    output_size = 4  # ラベル数( ラベル0, ラベル1, ラベル2, ラベル3)
    
    # 損失関数と最適化アルゴリズムを定義
    criterion = nn.CrossEntropyLoss()
    
    # 学習のループ
    num_epochs = 10
    
    # 学習データを格納するリスト
    data = []
    labels = []
    
    # 各ディレクトリ内のファイルを順番に処理
    for label_directory in os.listdir(main_data_directory):
        label = extract_label_from_directory(label_directory)  # ラベルをディレクトリ名から抽出
    
        # ラベルごとのデータディレクトリ
        data_directory = os.path.join(main_data_directory, label_directory)
    
        # データディレクトリ内のファイルを処理
        for filename in os.listdir(data_directory):
            # ファイル名からラベルを抽出
            label = extract_label_from_directory(label_directory)
            if label:
                # データファイルのパス
                file_path = os.path.join(data_directory, filename)
    
                # データの読み込みと前処理
                trajectory = list()  # データポイントをまとめ,軌跡データとするためのリスト
                with open(file_path, 'r') as file:  # ファイルを読み込み
                    for line in file:  # ファイルの各行に対して
                        parts = line.strip().split()  # 空白区切りのデータをsplitし
                        data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
                        trajectory.append(data_point)  # 軌跡データに追加
                data.append(trajectory)  # 軌跡データをデータリストに追加
                labels.append(label)  # ラベルを整数に変換してラベルリストに追加
    
    
    # 最小の軌跡データ長に合わせる前処理
    min_length = min(len(traj) for traj in data)
    data = [traj[:min_length] for traj in data]
    
    # データとラベルをテンソルに変換
    data = torch.tensor(data, dtype=torch.float32)
    labels = torch.tensor(labels, dtype=torch.long)
    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    
    # ニューラルネットワークモデルの初期化
    model = FixedNet(input_size, hidden_size, output_size)
    
    #トレーニング
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    for epoch in range(num_epochs):
        for data_batch, labels_batch in data_loader:
            optimizer.zero_grad()  # 勾配を初期化
            print(data_batch.shape)  # データバッチの形状を確認
            outputs = model(data_batch.permute(0, 2, 1))  # モデルの予測
            loss = criterion(outputs, labels_batch)  # クロスエントロピー損失を計算
            loss.backward()  # 逆伝播
            optimizer.step()  # パラメータの更新
    
    # 学習済みモデルを保存
    os.makedirs(model_directory, exist_ok=True)
    model_filename = "combined_model.pth"
    model_save_path = os.path.join(model_directory, model_filename)
    
    torch.save(model.state_dict(), model_save_path)
    
    # 新しいデータを予測
    new_data_file = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata/label3/coordinates_label3_20231016194808.txt"
    new_data = []
    
    with open(new_data_file, 'r') as file:
        for line in file:
            parts = line.strip().split()
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
            new_data.append(data_point)
    
    # 新しいデータのシーケンス長を最小の軌跡データ長に合わせる
    new_data = new_data[:min_length]
    
    # 新しいデータをPyTorchのテンソルに変換
    new_data = torch.tensor(new_data, dtype=torch.float32).view(1, -1, input_size)  # バッチサイズ x シーケンス長 x 入力次元
    
    # 学習済みモデルをロード
    model.load_state_dict(torch.load(model_save_path))
    model.eval()
    with torch.no_grad():
        output = model(new_data.permute(0, 2, 1))
        class_probabilities = torch.softmax(output, dim=2)
        predicted_class = torch.argmax(class_probabilities, dim=2).item()
        print(f"新しいデータの予測クラス: {predicted_class}")
    

    上記のプログラムは、これまでにご指摘して頂いたように、学習をすべてのラベルのデータに対して行うように変更してみました。また、データを読み込む際に3D座標データ1つ1つにラベル付けをしてしまっていることから、軌跡データとしてラベル付けするようにしました。さらに、データの入力の形状をファイルの行数x3にし、データ数を最小のものに合わせるコードを追加しました。

    このプログラムを実行すると、
    torch.Size([3, 172, 3])
    Traceback (most recent call last):
    File "/home/osumi/Python_test/test3.py", line 93, in
    outputs = model(data_batch.permute(0, 2, 1)) # モデルの予測
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/module.py", line 1501, in _call_impl
    return forward_call(*args, **kwargs)
    File "/home/osumi/Python_test/test3.py", line 15, in forward
    out, _ = self.lstm(x)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/module.py", line 1501, in _call_impl
    return forward_call(*args, **kwargs)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 810, in forward
    self.check_forward_args(input, hx, batch_sizes)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 730, in check_forward_args
    self.check_input(input, batch_sizes)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 218, in check_input
    raise RuntimeError(
    RuntimeError: input.size(-1) must be equal to input_size. Expected 3, got 172

    というエラーが発生してしまします。
    エラーが発生している理由は、input.size(-1)(入力の最後の次元)がinput_size(期待される入力の次元数)と一致しないことが原因だと述べられています。この場合だと、期待される入力の次元数は(x, y, z)の3であるべきですが、実際の入力データは最後の次元が172となっていることから、エラーが出ていると考えられます。ちなみに、この172とは、すべての軌跡データの中で最小のデータ数のことだとわかりました。

    このエラーから、LSTMモデルへの入力は (バッチサイズ, シーケンス長, 入力次元) の形状を持つ必要があるのに、data_batchは(バッチサイズ, 入力次元, シーケンス長) の形状になっていることが原因と考え、#トレーニングの中に、
    outputs = model(data_batch.permute(0, 2, 1))
    という、形状を合わせるコードを追加してみました。

    しかし、実行しても結果は変わらず、解決方法がよくわからなくなってしまいました。なにか、方法をご存知であれば教えて頂きたいです。また、他のコードに関しても何かあれば、ご指摘お願いします。

  11. 元々のdata_batch(batch_size, 172, 3)だったものを,data_batch.permute(0, 2, 1)で1番目と2番目を入れ替えて(batch_size, 3, 172)になったものと思われます.

    実際に直前のprint(data_batch.shape)torch.Size([3, 172, 3])ですよね.

    何かしら混乱してpermute(0, 2, 1)を入れてしまってtorch.Size([3, 3, 172])で次元不一致によるエラーで上の状態になったのではないでしょうか.

  12. @ayujaja

    Questioner

    おっしゃる通り、直前の、
    print(data_batch.shape)はtorch.Size([3, 172, 3])
    で、
    torch.Size([3, 172, 3])
    と表示されているため、入れ替えの必要は無いと考えたのですが、permute を入れなくても同じエラーが表示されてしまいます。

    また、何かしらの原因により、順番が違うのかなと思い、permuteで全6通りの順番に直してみても、
    ・期待されてる次元数「3」に対し、シーケンス長「172」になってしまう
    ・期待されてるバッチサイズが「172」に対し、「3」が入ってきている
    というエラーが発生してしまいます。

    これの原因が分からず、立ち往生してしまってます。なにか、原因となる箇所や間違いが分かりましたら、是非教えていただきたいです。

  13. なるほど,最初にあったエラーと類似していてそのままといった感じですね.
    permuteなしの場合どのエラーになるのか教えていただけますか?

    LSTMの引数にbatch_first=Trueが必要ですね.これがないと(batch_size, sequence_length, feature_length)に一致しません. 既にありましたね.失礼しました.

    現状,3Dなのとバッチサイズが3になっているシーンがあってどちらが原因かよくわからなくなってしまっている感じです.おそらく,最後の1バッチだけ動かない.とかだったりするのでしょうか.

    DataLoaderに対してdrop_last=Trueつけたらどうなりますか?

  14. ちなみに下記コードでdrop_last関係なく再現しません.

    import torch
    import torch.nn as nn
    from torch.utils.data import DataLoader, TensorDataset
    
    class SimpleLSTM(nn.Module):
        def __init__(self, input_dim, hidden_dim, output_dim=1, num_layers=1):
            super(SimpleLSTM, self).__init__()
            self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True)
            self.linear = nn.Linear(hidden_dim, output_dim)
        
        def forward(self, x):
            lstm_out, _ = self.lstm(x)
            output = self.linear(lstm_out[:, -1, :])
            return output
    
    batch_size = 32
    sequence_length = 10
    input_dim = 8
    
    data = torch.randn(batch_size * 10 + 2, sequence_length, input_dim)
    labels = torch.randint(0, 2, (batch_size * 10 + 2,))
    
    model = SimpleLSTM(input_dim, 50)
    criterion = nn.BCEWithLogitsLoss()
    optimizer = torch.optim.Adam(model.parameters())
    
    dataset = TensorDataset(data, labels)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)#, drop_last=True)
    
    # 訓練ループ
    for epoch in range(10):
        for inputs, targets in dataloader:
            model.train()
            optimizer.zero_grad()
            print(inputs.shape)
            outputs = model(inputs).squeeze()
            loss = criterion(outputs, targets.float())
            loss.backward()
            optimizer.step()
        print(f"Epoch {epoch+1}, Loss: {loss.item()}")
    
  15. @ayujaja

    Questioner

    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last = True)
    

    DataLoaderに、「drop_last=True」を入れ込んだところ、
    最小クラスインデックス: 0
    最大クラスインデックス: 3
    クラスの総数: 4
    データ 0 のサイズ: torch.Size([3, 146])
    データ 1 のサイズ: torch.Size([3, 146])
    データ 2 のサイズ: torch.Size([3, 146])
    ラベルのサイズ: torch.Size([3])
    Traceback (most recent call last):
    File "/home/osumi/Python_test/test3.py", line 147, in
    class_probabilities = torch.softmax(output, dim=2)
    IndexError: Dimension out of range (expected to be in range of [-2, 1], but got 2)
    というエラーが新たに出てきました。

  16. これはずっと気になってたことなんですが,結局のところファイル構造(treeコマンドの結果)はどういう感じになっているのでしょうか.
    件数が多いところは何件あるか明記いただいた上で省略して構いませんが,少なくともデータセット作成あたりで依然として何かおかしい気がします.

  17. @ayujaja

    Questioner

    今の状態としては、下記に示すように、メインディレクトリの中にlabel0,1,2,3の4つのディレクトリがあり、その中に軌跡ファイルが入っている状況です。メインディレクトリの中に、4ディレクトリ、4ファイルとなっています。

    openposedata
    ├── label0
    │ └── coordinates_label0_20231019125402.txt
    ├── label1
    │ └── coordinates_label1_20231016194432.txt
    ├── label2
    │ └── coordinates_label2_20231019163601.txt
    └── label3
    └── coordinates_label3_20231016194808.txt

    最初に質問したときは、軌跡データをラベルごとに5つほど保存していたのですが、今は1つずつしか残していません。
    ファイルが4つしかないにもかかわらずバッチサイズを3にしているように、バッチサイズに対して極端にデータが少ない状況がよろしくないのでしょうか?

  18. ファイルが4つしかないにもかかわらずバッチサイズを3にしているように、バッチサイズに対して極端にデータが少ない状況がよろしくないのでしょうか

    それは大いにありそうですね.drop_last=Trueを行う現状からすると,batch_size未満の件数しかないということはdrop_lastの対象となり,実質学習データはないものとしてDataLoaderが作られたようなものですね.とりあえず持ちうる全件使いつつdrop_last=TrueFalseでの両方の動作を調べる必要がありそうです.

    というか5件ずつ×4ラベルの20件は少ないですね.桁が1つ足りないです.

    また現状,extract_label_from_directoryでラベルを抽出しているようですが,その後のデータ読み出しループにあるif label:において,if 0:if False:if Noneと同じ動作を示します.つまり,ラベル01はなかったものとされていることもどこかしら影響がありそうです.

    1. int(directory_name.replace("label", "")) - 1にしているので,もしかしたらラベル1が0になり,ラベル0は-1になっているかもです.いずれかがなかったものになってそう.

  19. @ayujaja

    Questioner

    データ数をlabel0~3までで、37個ほど取得して、再度実行してみました。

    出力結果は、

    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
    最小クラスインデックス: 0
    最大クラスインデックス: 3
    クラスの総数: 4
    データ 0 のサイズ: torch.Size([3, 136])
    データ 1 のサイズ: torch.Size([3, 136])
    データ 2 のサイズ: torch.Size([3, 136])
    データ 3 のサイズ: torch.Size([3, 136])
    データ 4 のサイズ: torch.Size([3, 136])
    データ 5 のサイズ: torch.Size([3, 136])
    データ 6 のサイズ: torch.Size([3, 136])
    データ 7 のサイズ: torch.Size([3, 136])
    データ 8 のサイズ: torch.Size([3, 136])
    データ 9 のサイズ: torch.Size([3, 136])
    データ 10 のサイズ: torch.Size([3, 136])
    データ 11 のサイズ: torch.Size([3, 136])
    データ 12 のサイズ: torch.Size([3, 136])
    データ 13 のサイズ: torch.Size([3, 136])
    データ 14 のサイズ: torch.Size([3, 136])
    データ 15 のサイズ: torch.Size([3, 136])
    データ 16 のサイズ: torch.Size([3, 136])
    データ 17 のサイズ: torch.Size([3, 136])
    データ 18 のサイズ: torch.Size([3, 136])
    データ 19 のサイズ: torch.Size([3, 136])
    データ 20 のサイズ: torch.Size([3, 136])
    データ 21 のサイズ: torch.Size([3, 136])
    データ 22 のサイズ: torch.Size([3, 136])
    データ 23 のサイズ: torch.Size([3, 136])
    データ 24 のサイズ: torch.Size([3, 136])
    データ 25 のサイズ: torch.Size([3, 136])
    データ 26 のサイズ: torch.Size([3, 136])
    データ 27 のサイズ: torch.Size([3, 136])
    データ 28 のサイズ: torch.Size([3, 136])
    データ 29 のサイズ: torch.Size([3, 136])
    データ 30 のサイズ: torch.Size([3, 136])
    データ 31 のサイズ: torch.Size([3, 136])
    データ 32 のサイズ: torch.Size([3, 136])
    ラベルのサイズ: torch.Size([33])
    torch.Size([32, 3, 136])
    データバッチの形状: torch.Size([32, 3, 136])
    Traceback (most recent call last):
    File "/home/osumi/Python_test/test3.py", line 115, in
    outputs = model(data_batch)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/module.py", line 1501, in _call_impl
    return forward_call(*args, **kwargs)
    File "/home/osumi/Python_test/test3.py", line 15, in forward
    out, _ = self.lstm(x)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/module.py", line 1501, in _call_impl
    return forward_call(*args, **kwargs)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 810, in forward
    self.check_forward_args(input, hx, batch_sizes)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 730, in check_forward_args
    self.check_input(input, batch_sizes)
    File "/usr/local/lib/python3.8/dist-packages/torch/nn/modules/rnn.py", line 218, in check_input
    raise RuntimeError(
    RuntimeError: input.size(-1) must be equal to input_size. Expected 3, got 136
    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

    のようになりました。
    この出力結果より、データ数を増やすと、データバッチの形状がデータバッチの形状: torch.Size([32, 3, 136])となり、
    (バッチサイズ, 入力次元, シーケンス長)となることがわかりました。
    エラーに示されているように、やはりニューラルネットワークモデルへの入力データの形状が間違っているらしいです。
    再度、並べ替えて実行してみます。

    ちなみに、drop_lastをTrueとFalse両方でやっても出力結果は同じとなりました。

    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

    また、データ数が37個にもかかわらず、データが33個となっています。この理由がよくわからないのですが、

    label = int(directory_name.replace("label", "")) - 1  
    

    この部分の-1を-4以下にすると、データが37個になりました。

  20. なぜか(3, 136)になってますが,とりあえずこれに対してはbatch.permute(0, 2, 1)が答えなはずです.

    この部分の-1を-4以下にすると、データが37個になりました。

    「その後のデータ読み出しループにあるif label:において,if 0:if False:if Noneと同じ動作を示します.」と言った通り,-4以下にしたらlabel-1以下になるので(0にならないので),正しく全てのファイルを読み出せているといった状態です.

  21. @ayujaja

    Questioner

    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.utils.data import DataLoader, TensorDataset
    import os
    
    # ニューラルネットワークのモデルを定義 (LSTMを使用)
    class FixedNet(nn.Module):
        def __init__(self, input_size, hidden_size, num_classes):
            super(FixedNet, self).__init__()
            self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
            self.fc = nn.Linear(hidden_size, num_classes)
    
        def forward(self, x):
            out, _ = self.lstm(x)
            out = self.fc(out[:, -1, :])  # 最後の時刻の出力を使う
            return out
    
    # ディレクトリ名からラベル情報を抽出する関数
    def extract_label_from_directory(directory_name):
        # ディレクトリ名から "label" 接頭辞を削除して整数ラベルを取得
        label = int(directory_name.replace("label", "")) - 4  # ラベルを 0 から始めるように変更
        return label
    
    # データファイルのメインディレクトリ
    main_data_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata"
    
    # 学習済みモデルの保存ディレクトリ
    model_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク2"
    
    # ニューラルネットワークモデルの初期化
    input_size = 3  # 3D座標データの次元
    hidden_size = 64  # LSTMの隠れ層のサイズ
    output_size = 4  # ラベル数( ラベル0, ラベル1, ラベル2, ラベル3)
    
    # 損失関数と最適化アルゴリズムを定義
    criterion = nn.CrossEntropyLoss()
    
    # 学習のループ
    num_epochs = 100
    
    # 学習データを格納するリスト
    data = []
    labels = []
    
    # 各ディレクトリ内のファイルを順番に処理
    for label_directory in os.listdir(main_data_directory):
        label = extract_label_from_directory(label_directory)  # ラベルをディレクトリ名から抽出
    
        # ラベルごとのデータディレクトリ
        data_directory = os.path.join(main_data_directory, label_directory)
    
        # データディレクトリ内のファイルを処理
        for filename in os.listdir(data_directory):
            # ファイル名からラベルを抽出
            label = extract_label_from_directory(label_directory)
            if label:
                # データファイルのパス
                file_path = os.path.join(data_directory, filename)
    
                # データの読み込みと前処理
                trajectory = list()  # データポイントをまとめ,軌跡データとするためのリスト
                with open(file_path, 'r') as file:  # ファイルを読み込み
                    for line in file:  # ファイルの各行に対して
                        parts = line.strip().split()  # 空白区切りのデータをsplitし
                        data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
                        trajectory.append(data_point)  # 軌跡データに追加
                data.append(trajectory)  # 軌跡データをデータリストに追加
                labels.append(label)  # ラベルを整数に変換してラベルリストに追加
    
    # 最小の軌跡データ長に合わせる前処理
    min_length = min(len(traj) for traj in data)
    data = [traj[:min_length] for traj in data]
    
    # データとラベルをテンソルに変換
    data = torch.tensor(data, dtype=torch.float32)
    labels = torch.tensor(labels, dtype=torch.long)
    
    # データをバッチサイズ x シーケンス長 x 入力次元の形状に変更
    data = data.permute(0, 2, 1)
    
    # クラスのインデックスを調整
    min_index = labels.min().item()
    labels -= min_index
    
    # クラスの総数を修正
    num_classes = labels.max().item() + 1
    
    # 最小クラスインデックスと最大クラスインデックスを確認
    min_index = labels.min().item()
    max_index = labels.max().item()
    print(f"最小クラスインデックス: {min_index}")
    print(f"最大クラスインデックス: {max_index}")
    print(f"クラスの総数: {num_classes}")
    
    for i, traj in enumerate(data):
        print(f"データ {i} のサイズ: {traj.size()}")
    print(f"ラベルのサイズ: {labels.size()}")
    
    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last = True)
    
    # ニューラルネットワークモデルの初期化
    model = FixedNet(input_size, hidden_size, output_size)
    
    # トレーニング
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    for epoch in range(num_epochs):
        for data_batch, labels_batch in data_loader:
            optimizer.zero_grad()  # 勾配を初期化
            # データバッチの形状を修正する
            data_batch = data_batch.transpose(1, 2)  # シーケンス長の次元を2番目に移動
            outputs = model(data_batch)
            loss = criterion(outputs, labels_batch)  # クロスエントロピー損失を計算
            loss.backward()  # 逆伝播
            optimizer.step()  # パラメータの更新
    
    
    
    
    # 学習済みモデルを保存
    os.makedirs(model_directory, exist_ok=True)
    model_filename = "combined_model.pth"
    model_save_path = os.path.join(model_directory, model_filename)
    torch.save(model.state_dict(), model_save_path)
    
    # 新しいデータを予測
    new_data_file = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata/label0/coordinates_label0_20231020153003.txt"
    new_data = []
    
    with open(new_data_file, 'r') as file:
        for line in file:
            parts = line.strip().split()
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
            new_data.append(data_point)
    
    
    # 新しいデータのシーケンス長を最小の軌跡データ長に合わせる
    new_data = new_data[:min_length]
    
    # 新しいデータをPyTorchのテンソルに変換
    new_data = torch.tensor(new_data, dtype=torch.float32).view(1, -1, input_size)  # バッチサイズ x シーケンス長 x 入力次元
    
    # 学習済みモデルをロード
    model.load_state_dict(torch.load(model_save_path))
    model.eval()
    with torch.no_grad():
        output = model(new_data)
        class_probabilities = torch.softmax(output, dim=1)
        predicted_class = torch.argmax(class_probabilities, dim=1).item()
        print(f"新しいデータの予測クラス: {predicted_class}")
    

    プログラムをこのように修正しました。
    修正箇所は、
    1.-4にして全データを読み込む

    label = int(directory_name.replace("label", "")) - 4  # ラベルを 0 から始めるように変更
    

    2.モデルは、最終的な出力を取得するためにout[:, -1, :]を使用しています。したがって、モデルの出力の形状は (バッチサイズ, シーケンス長, hidden_size) から (バッチサイズ, hidden_size)となるため、dim=2をdim=1に変更

    predicted_class = torch.argmax(class_probabilities, dim=1).item()
    

    これらの修正を行うと、下記の出力結果を得ました

    最小クラスインデックス: 0
    最大クラスインデックス: 3
    クラスの総数: 4
    データ 0 のサイズ: torch.Size([3, 136])
    データ 1 のサイズ: torch.Size([3, 136])
    データ 2 のサイズ: torch.Size([3, 136])
    データ 3 のサイズ: torch.Size([3, 136])
    データ 4 のサイズ: torch.Size([3, 136])
    データ 5 のサイズ: torch.Size([3, 136])
    データ 6 のサイズ: torch.Size([3, 136])
    データ 7 のサイズ: torch.Size([3, 136])
    データ 8 のサイズ: torch.Size([3, 136])
    データ 9 のサイズ: torch.Size([3, 136])
    データ 10 のサイズ: torch.Size([3, 136])
    データ 11 のサイズ: torch.Size([3, 136])
    データ 12 のサイズ: torch.Size([3, 136])
    データ 13 のサイズ: torch.Size([3, 136])
    データ 14 のサイズ: torch.Size([3, 136])
    データ 15 のサイズ: torch.Size([3, 136])
    データ 16 のサイズ: torch.Size([3, 136])
    データ 17 のサイズ: torch.Size([3, 136])
    データ 18 のサイズ: torch.Size([3, 136])
    データ 19 のサイズ: torch.Size([3, 136])
    データ 20 のサイズ: torch.Size([3, 136])
    データ 21 のサイズ: torch.Size([3, 136])
    データ 22 のサイズ: torch.Size([3, 136])
    データ 23 のサイズ: torch.Size([3, 136])
    データ 24 のサイズ: torch.Size([3, 136])
    データ 25 のサイズ: torch.Size([3, 136])
    データ 26 のサイズ: torch.Size([3, 136])
    データ 27 のサイズ: torch.Size([3, 136])
    データ 28 のサイズ: torch.Size([3, 136])
    データ 29 のサイズ: torch.Size([3, 136])
    データ 30 のサイズ: torch.Size([3, 136])
    データ 31 のサイズ: torch.Size([3, 136])
    データ 32 のサイズ: torch.Size([3, 136])
    データ 33 のサイズ: torch.Size([3, 136])
    データ 34 のサイズ: torch.Size([3, 136])
    データ 35 のサイズ: torch.Size([3, 136])
    データ 36 のサイズ: torch.Size([3, 136])
    ラベルのサイズ: torch.Size([37])
    新しいデータの予測クラス: 3

    とりあえず、エラーは消えたのですが、やはり予測結果が全く合いません。
    私の考えとしては、新しいデータを軌跡として捉える前処理を行えていないからでは、と考えているのですが、他に理由があったりするのでしょうか。

  22. なるほど,

    # データをバッチサイズ x シーケンス長 x 入力次元の形状に変更
    data = data.permute(0, 2, 1)
    

    のコメント,正しくは「バッチサイズ x 入力次元 x シーケンス長の形状に変更」ですね.この処理は不要です.おそらくPandasとかから読み出したデータはこう変換しないといけなかったりしますが今回は不要な処理です.これを消したらdata_batch = data_batch.transpose(1, 2)も不要です.

    やはり予測結果が全く合いません。私の考えとしては、新しいデータを軌跡として捉える前処理を行えていないからでは、と考えている

    学習曲線を見てないからわかりませんが,未学習もしくは過学習のどちらかですね.データ数が少ないのでどちらが要因か問題を切り分けることはできなさそうです.

    ひとまず,正規化の前処理がないのが原因かなと思います.データ数が少ない現状はdata augmentationで対処しましょう.

    入力値の範囲が少し異なるだけで,大きく予測ラベルを変化させることが可能なのでとりあえず前処理でなんとかするしかないです.

  23. @ayujaja

    Questioner

    わかりました。
    ひとまず、data augmentationを用いて、データを拡張してやってみます。

    また、1つ質問なのですが、色々なデータで予測してみると、ほとんどがクラス3(たまに2)に分類されます。データの数が、label3が一番多く、次に2が多いのですが、これが関係しているのでしょうか?

  24. data augmentationはぜひやってみてください.データ数も少ないので,online data augmentationがおすすめです.

    ほとんどがクラス3(たまに2)に分類されます。データの数が、label3が一番多く、次に2が多い

    imbalanced dataでは,多いクラスを予測値として叩き出す出す方が,モデルにとってお得です(正解率が高くなるので).

    ひとまずclass weightをかけて少ないクラスを重点的に学習させると改善されるというようになっています.

    こういうときは,まずConfusion Matrix1を描いてモデルの性能と見つめ合うことになります.次この話題になるときはぜひ持ってきてください.

    「ほとんどがクラス3(たまに2)に分類されます。データの数が、label3が一番多く、次に2が多い」という文章は,このConfusion Matrixで定量的に説明されます.

    1. 例えばこんなのです.Confusion Matrixでググるとよく2値分類のものが出てきますが,今回はマルチクラス分類なので気をつけてください.

  25. @ayujaja

    Questioner

    ありがとうございます、online data augmentationでやってみます。

    Confusion Matrixですね、見たことはありますが、こういうときに用いるとわかりやすく定量的に説明できるのですね。非常に勉強になります。

  26. そうですね,回答する側としてはありがたいです.
    ちなみにpermuteの問題に関して,直近に投稿いただいたコードだけでは事前にpermuteされているのがわからなかったので,適宜更新等していただけるとありがたいです.

  27. @ayujaja

    Questioner

    permuteの問題で指摘された、

    data = data.permute(0, 2, 1)
    

    data_batch = data_batch.transpose(1, 2)
    

    を両方消したのですが、特に問題なさそうです。#トレーニングの中でprintでデータの形状を確認すると、
    データバッチの形状: torch.Size([32, 136, 3])
    となっています。

  28. そうですね,特に変なことしなければ時系列解析用のデータ形式になっているはずでした.こちらもすっきりしましたありがとうございます.

  29. @ayujaja

    Questioner

    何度もすみません。online data augmentationについて調べ、実践しようとしたところ、
    ・ノイズの追加 (Adding Noise)
    ・ランダムなシフト (Random Shift)
    ・ランダムな回転 (Random Rotation)
    ・タイムシフト (Time Shift)
    などの種類があると知り、どれが軌跡データに対して適しているのか、是非意見を聞かせていただきたいです。

  30. Random ShiftとTime Shiftはこの場合ほとんど同じ扱いで,Random Rotationは画像以上のランクを持つデータで使えますので,今回は使えませんね1

    一番やりやすいのは

    • ノイズの追加 (Adding Noise)
    • 移動平均 (Moving Average)

    になります.

    少し頑張れば,例えば今回ファイルのサイズがそれぞれ異なるということで,最小行数ファイルはどうしようもありません2が,そうでないファイルはRandom Shiftできます.余剰行を利用して毎回抽出するデータを変更できますね.

    次にめんどいのは剰余行のある特性を生かして,$\text{sequence_length} \pm n$行のデータを$\text{sequence_length}$行まで伸縮させたりする方法もあります.$n$は$\text{sequence_length}$に対してより小さい値(今回であれば136に対して1~2ぐらい)の変動は動作が早くなったり遅くなったりしても誤差であるとして波形の微調整を行ったりするものです3

    1. 時間を逆転させるのもRotationではあります.今回の解析対象の映像を逆再生する動きが自然の摂理にかなっているのであればやってもいいと思います.画像でも左右反転や数~数十度回転まではやりますが,上下反転はやらないのと同じ感じです.

    2. これは少し誤りがありますが,実データだけでShiftできない事実に変わりはありません.任意の値をpaddingすることでデータの長さを揃える処理を行う前提で,最小行数のファイルもRandom Shiftできます.定数値をpaddingするのが一番簡単ですが,今回であれば補外しても良いと思います.

    3. 画像処理で言うところのRandom Warpingと同じ効果になります.

  31. @ayujaja

    Questioner

    すみません、またわからないことが出てきてしまったので、質問させて頂きます。お時間あれば、答えていただきたいです。

    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.utils.data import DataLoader, TensorDataset
    from sklearn.utils.class_weight import compute_class_weight
    import numpy as np
    import os
    from torchviz import make_dot
    
    # ニューラルネットワークのモデルを定義 (LSTMを使用)
    class FixedNet(nn.Module):
        def __init__(self, input_size, hidden_size, num_classes):
            super(FixedNet, self).__init__()
            self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
            self.fc = nn.Linear(hidden_size, num_classes)
    
        def forward(self, x):
            out, _ = self.lstm(x)
            out = self.fc(out[:, -1, :])  # 最後の時刻の出力を使う
            return out
    
    # ディレクトリ名からラベル情報を抽出する関数
    def extract_label_from_directory(directory_name):
        # ディレクトリ名から "label" 接頭辞を削除して整数ラベルを取得
        label = int(directory_name.replace("label", "")) - 4  # ラベルを 0 から始めるように変更
        return label
    
    # データファイルのメインディレクトリ
    main_data_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/openposedata"
    
    # 学習済みモデルの保存ディレクトリ
    model_directory = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク2"
    
    # ニューラルネットワークモデルの初期化
    input_size = 3  # 3D座標データの次元
    hidden_size = 64  # LSTMの隠れ層のサイズ
    output_size = 4  # ラベル数( ラベル0, ラベル1, ラベル2, ラベル3)
    
    
    # 保存先ディレクトリを指定
    save_dir = "/home/osumi/catkin_ws/src/ros_openpose-master/ニューラルネットワーク"
    # モデルの可視化
    model = model = FixedNet(input_size, hidden_size, output_size)
    
    x = torch.rand(1, 10, input_size)  # ダミー入力データ (バッチサイズ 1, シーケンス長 10, 入力次元 3)
    out = model(x)
    dot = make_dot(out, params=dict(model.named_parameters()))
    # ファイルのフルパスを作成
    save_path = os.path.join(save_dir, "my_model")
    dot.render(save_path, format="png")  # 正しいファイルパスを使用して可視化をPNGファイルとして保存
    
    
    # 学習データを格納するリスト
    data = []
    labels = []
    
    # 各ディレクトリ内のファイルを順番に処理
    for label_directory in os.listdir(main_data_directory):
        label = extract_label_from_directory(label_directory)  # ラベルをディレクトリ名から抽出
    
        # ラベルごとのデータディレクトリ
        data_directory = os.path.join(main_data_directory, label_directory)
    
        # データディレクトリ内のファイルを処理が右側に動かした軌跡です。
        for filename in os.listdir(data_directory):
            # ファイル名からラベルを抽出
            label = extract_label_from_directory(label_directory)
            if label:
                # データファイルのパス
                file_path = os.path.join(data_directory, filename)
    
                # データの読み込みと前処理
                trajectory = list()  # データポイントをまとめ,軌跡データとするためのリスト
                with open(file_path, 'r') as file:  # ファイルを読み込み
                    for line in file:  # ファイルの各行に対して
                        parts = line.strip().split()  # 空白区切りのデータをsplitし
                        data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
                        trajectory.append(data_point)  # 軌跡データに追加
                data.append(trajectory)  # 軌跡データをデータリストに追加
                labels.append(label)  # ラベルを整数に変換してラベルリストに追加
    
    # 最小の軌跡データ長に合わせる前処理
    min_length = min(len(traj) for traj in data)
    data = [traj[:min_length] for traj in data]
    
    # データとラベルをテンソルに変換
    data = torch.tensor(data, dtype=torch.float32)
    labels = torch.tensor(labels, dtype=torch.long)
    
    # クラスのインデックスを調整
    min_index = labels.min().item()
    labels -= min_index
    
    # クラスの総数を修正
    num_classes = labels.max().item() + 1
    
    # 最小クラスインデックスと最大クラスインデックスを確認
    min_index = labels.min().item()
    max_index = labels.max().item()
    print(f"最小クラスインデックス: {min_index}")
    print(f"最大クラスインデックス: {max_index}")
    print(f"クラスの総数: {num_classes}")
    
    
    print(f"ラベルのサイズ: {labels.size()}")
    
    # DataLoaderを作成
    batch_size = 32
    dataset = TensorDataset(data, labels)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
    
    # クラスの重みを計算
    
    
    # ニューラルネットワークモデルの初期化
    model = FixedNet(input_size, hidden_size, output_size)
    
    # モデルの構造を可視化
    dummy_input = torch.randn(1, min_length, input_size)  # ダミーの入力を生成
    make_dot(model(dummy_input), params=dict(model.named_parameters())).render("fixednet_model", format="png")  # "fixednet_model" は可視化の出力ファイル名
    
    # トレーニング
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    criterion = nn.CrossEntropyLoss()  # 重みの計算を省いた損失関数
    
    num_epochs = 100
    for epoch in range(num_epochs):
        for data_batch, labels_batch in data_loader:
            optimizer.zero_grad()  # 勾配を初期化
    
            # オンラインデータ拡張: ノイズを追加
            noise_level = 0.001  # ノイズの大きさを設定
            noise = torch.randn(data_batch.size()) * noise_level
            data_batch = data_batch + noise
    
            # データ正規化
            mean = data_batch.mean()
            std = data_batch.std()
            data_batch = (data_batch - mean) / std
    
            outputs = model(data_batch)
            loss = criterion(outputs, labels_batch)  # クロスエントロピー損失を計算
            loss.backward()  # 逆伝播
            optimizer.step()  # パラメータの更新
    
    
    
    
    # 学習済みモデルを保存
    os.makedirs(model_directory, exist_ok=True)
    model_filename = "combined_model.pth"
    model_save_path = os.path.join(model_directory, model_filename)
    torch.save(model.state_dict(), model_save_path)
    
    # 新しいデータファイルを読み込む
    new_data_file = "/home/osumi/catkin_ws/src/ros_openpose-master/テスト用座標データ/coordinates_label0_20231022125615.txt"
    new_data = []
    
    with open(new_data_file, 'r') as file:
        trajectory = []  # 新しい軌跡データを格納するリスト
        for line in file:
            parts = line.strip().split()
            data_point = [float(parts[0]), float(parts[1]), float(parts[2])]
            trajectory.append(data_point)  # 各データポイントを軌跡に追加
        new_data.append(trajectory)  # 軌跡データを新しいデータリストに追加
    
    # 新しいデータのシーケンス長を最小の軌跡データ長に合わせる
    new_data = new_data[:min_length]
    
    # 新しいデータをPyTorchのテンソルに変換
    new_data = torch.tensor(new_data, dtype=torch.float32).view(1, -1, input_size)  # バッチサイズ x シーケンス長 x 入力次元
    
    # 学習済みモデルをロード
    model.load_state_dict(torch.load(model_save_path))
    model.eval()
    with torch.no_grad():
        output = model(new_data)
    
    # ノイズの追加
    noise_level = 0.001  # ノイズの大きさを設定
    noise = torch.randn(new_data.size()) * noise_level
    new_data = new_data + noise
    
    # データ正規化
    mean = new_data.mean()
    std = new_data.std()
    new_data = (new_data - mean) / std
    
    # 予測を実行
    class_probabilities = torch.softmax(output, dim=1)
    predicted_class = torch.argmax(class_probabilities, dim=1).item()
    
    
    print(f"モデルの出力: {output}")
    print(f"クラス確率: {class_probabilities}")
    print(f"新しいデータの予測クラス: {predicted_class}")
    

    以前指摘していただいた、データ拡張や正規化を含め、プログラムを修正したのですが、こちらのコードでも予測がなかなかうまく行きません。ハイパーパラメータをいじったりもしましたが、あまり良い変化はありませんでした。

    また、予測結果に偏りがあったため、データ数を同じにして重みを考慮する部分を消しました。しかし、こちらも結果は変わりませんでした。

    これは、単にデータ数が少ないことによる、未学習や過学習が原因なのでしょうか?ちなみに、データ数は、4種類の軌跡ごとに50通り取得し、合計で200ほど集めてみました。

    お手数ですが、再度コードを確認していただき、原因について一緒に考えていただけると幸いです。

    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
    出力結果を忘れていました。

    最小クラスインデックス: 0
    最大クラスインデックス: 3
    クラスの総数: 4
    ラベルのサイズ: torch.Size([200])
    モデルの出力: tensor([[ 3.0266,  2.1688, -4.7794, -1.8982]])
    クラス確率: tensor([[6.9845e-01, 2.9619e-01, 2.8444e-04, 5.0734e-03]])
    新しいデータの予測クラス: 0
    

    こんな感じです。補足ですが、クラス1と2、クラス3と4で、それぞれ似た軌跡データとなっています。

  32. 次の箇所に問題がありそうです.

    # オンラインデータ拡張: ノイズを追加
    noise_level = 0.001  # ノイズの大きさを設定
    noise = torch.randn(data_batch.size()) * noise_level
    data_batch = data_batch + noise
    
    # データ正規化
    mean = data_batch.mean()
    std = data_batch.std()
    data_batch = (data_batch - mean) / std
    

    まず,先にノイズを追加してしまっています.これでは値の範囲が不明なものに対して$\sigma=0.001$のノイズを追加しています.OpenPoseでは特にオプションが無ければ,画像のピクセル数のスケールでデータが得られるものと認識しております.オプション付加してようやく$[0, 1]$の値域だったはずです.特に前者の状態に対して$\pm 0.001$することは,比率で考えてほとんど変動の無い値を追加していることになってしまいます.

    普通は正規化したあとに値域を揃えてノイズを追加するものです.標準偏差が1に対して1%の誤差を与えたいから$0.001$という筋は通りますが現状ではそんなの無いですよね.

    次にデータの正規化ですが,これはバッチ正規化を行っていますね.他の正規化を試したのか気になるところです.

    • Sample-wise Normalization: (batch_data - batch_data.mean(axis=(1,2), keepdim=True)) / batch_data.std(axis=(1,2), keepdim=True)
    • Channel-wise Normalization: (batch_data - batch_data.mean(axis=(1,), keepdim=True)) / batch_data.std(axis=(1,), keepdim=True)

    Z-Score Normalization後は分散及び標準偏差が共に$(\sigma^2, \sigma)=(1, 1)$であることが保証されるので,これに対してノイズを1%加えるというのは筋が通りますし,正しくノイズを付加できていると言えるでしょう.

    予測結果に偏りがあったため、データ数を同じにして重みを考慮する部分を消しました。しかし、こちらも結果は変わりませんでした。これは、単にデータ数が少ないことによる、未学習や過学習が原因なのでしょうか?ちなみに、データ数は、4種類の軌跡ごとに50通り取得し、合計で200ほど集めてみました

    上のリンクにもある通り,

    サンプル数が無限にあれば理論上バイアスもバリアンスもゼロにできる。

    というのが理想ですが,データが少ない場合は,モデルの性能を上下させて,トレードオフの関係にある未学習と過学習の中間を目指すことになります.

    これを知るためには,まずデータを学習用/検証用/テスト用に分けて,クロスバリデーションを実施しましょう.

    そのうえでパラメータチューニングを行います.

    手動でチューニングを行うのではなく,tunerを使って実施するのをおすすめします.

    NNを使ったモデルのチューニング対象は多く,今回の簡単なモデルだけでも

    • LSTM: GRUどころかCNNを使う選択肢もある
      • hidden_size: 現状64.減らせば過学習抑制になるものの未学習になる可能性あり.増やせば未学習状態を改善できるものの過学習に繋がる恐れあり.
      • num_layers: デフォルト1.増やせば増やすほど性能が上がる(過学習しやすくなる).
      • bias: デフォルトTrue.これをFalseにする選択肢もある.
      • dropout: デフォルト最低値0.0が設定されている.増やせば増やすほど正則化に繋がるとされているものの,性能不足になり未学習になる可能性あり.
    • batch_size: 現状の32じゃなくても良いかもしれない.大きいと損失関数が学習データの状態を反映しすぎて汎化性能に欠ける状態もあり得る.
    • optim.Adam(): Adam以外の選択肢もある
      • lr: 学習率.デフォルト0.001.学習初期は0からwarm upして徐々に減少させるなどのノウハウ1もある.
      • betas: デフォルト(0.9, 0.999).どっちもチューニング対象.
      • weight_decay: デフォルト0.0より大きい値を与えればAdamWと同じになるはずだった気がするかも

    などなど,全て網羅しようとするとパターン数は膨大な数になりますね.先のリンクでも使われているベイズ最適化に頼れば比較的短時間で良さそうな結果を出してくれるはずです.

    さて,これの実施を目的として学習用/検証用/テスト用に分けてCross Validationの実装から始めましょう.先のリンクにあるような図

    img

    のように,モデルのパラメータを向上させて調べることになります.検証用データの状態が学習用データとの評価指標からかけ離れていく瞬間が最適なモデルのパラメータということになりますね.

    この図を描画して初めて,今あるデータでの精度の限界を知ることができます.まずは簡単に,この図の横軸としてhidden_sizeを16ぐらいから徐々に増やしていって確認するのも良いですし,num_layersを1から徐々に増やして確認するのも良いでしょう2.縦軸はF1 Scoreぐらいが良いと思います.

    めんどくさい,と思われた場合はまず,今あるFixedNetでいうところのout[:, -1, :](最悪dataそのものでも良い)の値を全てt-SNEにかけてみて,得られた分布が視覚的に分類可能かどうか調べるとよいでしょう.

    例えば手書き数字のデータセット(1画像につき27x27ピクセル=計729ピクセル)をt-SNEにかけると次の3次元空間に分布します.

    同様に,英単語もEmbeddingして次のように分布されます(他の例もあります).

    今回のデータは136x3=408なので,簡単に分類できるのであれば生のデータを入れるだけで例のようにわかりやすい分布になるはずです.これでも4クラスの境界がわかりづらければout[:, -1, :]で得られる値をt-SNEで確認したらよいでしょう.データの量の少なさor質の悪さ次第ではt-SNEで4クラス識別できないぐらい乱雑な分布になるかもしれません.

    1. https://www.baeldung.com/cs/learning-rate-warm-up

    2. パラメータ数基準で言えば,num_layersを$n$倍に増やすことは,hidden_sizeを$n$倍にすることと等しくなるはずです.

  33. @ayujaja

    Questioner

    クロスバリデーションを追加したところ,とりあえず正しく判断してくれるようになりました.
    これから, 学習データを増やしたり最適なパラメータチューニングを行い,精度を上げていこうと思います.色々と詳しく教えてくださり,本当にありがとうございました.

    また何かあれば,質問させていただくかもしれません.もし,お時間あればお答えしていただけると幸いです.

  34. データはあればあるだけ良いですからね.ぜひ注力してください.
    またこれに関して何かあったときは,この質問へのリンクを付加しつつ新しく質問を立ててくれると助かります.

Your answer might help someone💌