ChatGPTにlibtorchでのテンソル正規化について質問してみた

 ChatGPTにlibtorchでテンソルの正規化について質問してみた。
 質問文に対して少しズレた答えである上に、テンソルの割り算部分で形状に互換性がなく、エラーの出るコードが返ってきた。
 やはり鵜呑みには出来ないが、エラー箇所を具体的にChatGPTがわかるように質問すると割りと有用な修正のための答えが返ってくる。

You
Libtorchで二種類のスケールを持つ入力を正規化する前処理は?
ChatGPT
二種類のスケールを持つ入力を正規化する前処理には、通常、以下の手順が含まれます。

各入力チャンネルごとに異なる平均と標準偏差を使用して各チャンネルを正規化します。
各チャンネルの値を所定の範囲にクリッピングします(たとえば、0から1の範囲)。
以下は、二種類のスケールを持つ入力を正規化するための基本的なC++のコード例です。
#include <torch/torch.h>

// 二種類のスケールを持つ入力を正規化する関数
torch::Tensor normalizeInput(torch::Tensor input) {
    // 平均と標準偏差を計算する(各チャンネルごと)
    torch::Tensor mean = input.mean({2, 3}, true);
    torch::Tensor std = input.std({2, 3}, true);

    // 各チャンネルごとに正規化する
    input = (input - mean) / std;

    // 各チャンネルの値をクリッピングする(0から1の範囲に)
    input = torch::clamp(input, 0, 1);

    return input;
}

int main() {
    // 入力テンソルの準備(仮のデータ)
    torch::Tensor input = torch::randn({1, 3, 256, 256});

    // 入力を正規化する
    torch::Tensor normalized_input = normalizeInput(input);

    return 0;
}
この例では、normalizeInput関数が入力テンソルを正規化するために使用されています。入力テンソルがtorch::Tensorとして与えられ、各チャンネルごとの平均と標準偏差が計算され、入力がそれらで正規化されます。最後に、入力が0から1の範囲にクリッピングされます。

 上のコードでは割り算でエラーが出るので、再度質問する。具体的に質問する点が重要。

You
libtorchでCPUFloatType{1,2,1,1}をCPUFloatType{2}で割るには?
ChatGPT
Libtorchでは、テンソル同士の割り算を行う際には、ブロードキャスト(Broadcasting)を使用して形状を合わせる必要があります。CPUFloatType{1,2,1,1}とCPUFloatType{2}という形状を持つテンソル同士を割り算する場合、CPUFloatType{2}をCPUFloatType{1,1,1,2}にブロードキャストすることで、形状を一致させることができます。

以下は、Libtorchを使用してCPUFloatType{1,2,1,1}をCPUFloatType{2}で割る例です。
#include <torch/torch.h>

int main() {
    // 分子となるテンソルを作成(形状: {1, 2, 1, 1})
    torch::Tensor tensor1 = torch::ones({1, 2, 1, 1}, torch::kFloat32);
    // 分母となるテンソルを作成(形状: {2})
    torch::Tensor tensor2 = torch::ones({2}, torch::kFloat32) * 2;

    // 分母の形状を分子と同じにするためにブロードキャストする
    tensor2 = tensor2.unsqueeze(0).unsqueeze(2).unsqueeze(3);  // 形状を {1, 1, 1, 2} に変更

    // テンソルの割り算
    torch::Tensor result = tensor1 / tensor2;

    // 結果の表示
    std::cout << "Result Tensor:" << std::endl;
    std::cout << result << std::endl;

    return 0;
}
このコードでは、unsqueeze関数を使用してCPUFloatType{2}のテンソルをCPUFloatType{1,1,1,2}にブロードキャストし、それから割り算を行っています。これにより、テンソル同士の形状を一致させて割り算を行うことができます。

 この答えも、形状変更後のテンソルの形状が異なっている気がする。ソースコードでやっている事の説明とは異なるからだ。


 結果として、以下の様に割るテンソルの形状をブロードキャストしてやることで所望の結果が得られる。

torch::Tensor in = torch::rand({ 1,2,3,3 });
torch::Tensor mean = in.mean({ 0,2,3 }, true);// CPUFloatType{1,2,1,1}
torch::Tensor sd = in.std({ 0,2,3 }, false);// CPUFloatType{2}
sd = sd.unsqueeze(0).unsqueeze(2).unsqueeze(3);// broadcast to CPUFloatType{1,2,1,1}
in = (in - mean) / sd;
in = torch::clamp(in, 0, 1);

libtorchの学習にはリファレンスやドキュメントよりChatGPTを優先して使う

 libtorchのリファレンスや公式チュートリアルサイトはあるにはあるのだが、良い学習効率が得られるとは思えないので、ChatGPTを使ってみた。
 「libtorchで何々をするには?」と聞くだけでサンプルコードと説明をずらずらっと出してくれる。
 以下のような質問に対し、ChatGPTの返答はこうなった。内容が誤っていることもあるので注意は必要だが、大体の事がわかるというだけで非常に有り難い。

自分
libtorchでCSVから入力データを作るには?
ChatGPT
LibTorchを使用してCSVファイルから入力データを読み込む方法はいくつかあります。基本的な手法の1つは、C++のファイル入出力機能を使用してCSVファイルを読み込み、データをテンソルに変換することです。以下はその例です。
#include <torch/torch.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>

// CSVファイルからデータを読み込み、テンソルに変換する関数
torch::Tensor readCSVToTensor(const std::string& filename, const int numColumns) {
    std::ifstream file(filename);
    std::string line;
    std::vector<float> data;
    while (std::getline(file, line)) {
        std::stringstream ss(line);
        std::string cell;
        while (std::getline(ss, cell, ',')) {
            data.push_back(std::stof(cell));
        }
    }
    file.close();

    // データをテンソルに変換
    auto options = torch::TensorOptions().dtype(torch::kFloat32);
    torch::Tensor tensor = torch::from_blob(data.data(), {static_cast<long>(data.size() / numColumns), numColumns}, options);

    return tensor.clone(); // コピーして返す
}

int main() {
    // CSVファイルからデータを読み込み
    std::string filename = "data.csv"; // 適切なファイルパスに変更してください
    torch::Tensor input_data = readCSVToTensor(filename, 3); // 3はCSVの列数を表します

    // 入力データの表示
    std::cout << "Input Data:\n" << input_data << std::endl;

    return 0;
}
この例では、readCSVToTensor()関数を使用して、指定されたCSVファイルからデータを読み込み、それをテンソルに変換しています。CSVファイルはコンマ区切りとして扱われ、各行のデータは1行ごとに読み込まれ、それぞれの要素はカンマで区切られます。読み込まれたデータは、torch::from_blob()関数を使用して、直接テンソルに変換されます。
データの形状はCSVファイルの列数と、各列のデータ数に基づいて自動的に決定されます。この例では、CSVファイルの列数が3と仮定していますが、必要に応じて変更してください。

 プロンプターが人海戦術的に学習させていなければChatGPTと言えどもさすがに今のようなクオリティにはならないのだが、Cookbook系をわざわざ開く機会は間違いなく減るだろう。
 

libtorchにおけるテンソルのスライスやリダクションが分かりづらい時

 libtorchにおいて3次元以上のスライスで-1を指定し、テンソルをリダクションする時の記法で迷ったので練習。

using torch::indexing::Slice;
using torch::indexing::None;
using torch::indexing::Ellipsis;
//3階テンソル
torch::Tensor t = torch::arange(18).view({ 3,2,3 });
cout << "t:\n" << t << endl;;
//0次元と2次元の配列番号は網羅、1次元は最終配列番号のみ参照
//3x3の2階テンソルになり、1,2次元で構成される行列の行列方向はリダクション時に維持される
cout << "t[:,-1]:\n" << t.index({ Slice(), -1 }) << endl;
//0,1次元の配列番号は網羅、2次元は最終配列番号のみ参照
//3x2の2階テンソルになり、1,2次元で構成される行列の行列方向はリダクション時に維持されない
cout << "t[:,:,-1]:\n" << t.index({ Slice(), Slice(), -1 }) << endl;
//0次元の配列番号のみ網羅、1,2次元は最終配列番号のみ参照
//1,2次元で構成される行列の最終要素のみを抽出し3次元ベクトルに
cout << "t[:,-1,-1]:\n" << t.index({ Slice(), -1, -1 }) << endl;
//1,2次元の配列番号を網羅し、0次元の最終配列要素を参照
//0次元の最終要素の1,2次元で構成される行列を抽出し2x3の2階テンソルに
//リダクション時に1,2次元で構成される行列の行列方向はリダクション時に維持される
cout << "t[-1,:]:\n" << t.index({ -1,Slice() }) << endl;
cout << "t[-1,:,:]:\n" << t.index({ -1,Slice(),Slice() }) << endl;//上と同じ結果になる
//2次元の配列番号を網羅し、0,1次元の最終配列要素を参照
//0次元最終要素の1,2次元で構成される行列の最終行のみを抽出し3次元ベクトルに
cout << "t[-1,-1,:]:\n" << t.index({ -1,-1,Slice() }) << endl;
t:
(1,.,.) =
	0  1  2
	3  4  5

(2,.,.) =
	 6   7   8
	 9  10  11

(3,.,.) =
	12  13  14
	15  16  17
[ CPULongType{3,2,3} ]
t[:,-1]:
	3   4   5
	9  10  11
	15  16  17
[ CPULongType{3,3} ]
t[:,:,-1]:
	2   5
	8  11
	14  17
[ CPULongType{3,2} ]
t[:,-1,-1]:
	5
	11
	17
[ CPULongType{3} ]
t[-1,:]:
 	12  13  14
 	15  16  17
[ CPULongType{2,3} ]
t[-1,-1,:]:
 	15
 	16
 	17
[ CPULongType{3} ]

 特に忘れやすいのは、ixjxkの3階テンソルで網羅する配列番号の範囲を指定するとき、t[:,-1](t.index({ Slice(), -1 }))、などと記述すると、jについてのみ最終配列番号を参照しiとkについては網羅するという意味になったりする点だろうか。
 また、view関数とは違い、要素を保持したままの自動形状変更ではなく切り抜きである。
 スライスは何度やっても忘れる非常に問題のある記法だと思う。

Visual Studio 2022でDiagnostic Toolsが動かない時

  • VSインストーラーから.NET開発環境(診断ツールは必須)を入れておく
  • Visual Studio Standard Collector Serviceが開始されていることをWindowsサービスで確認する 

learn.microsoft.com
 他のソフトウェアからの操作やVSのアンインストールなどが影響してVisual Studio Standard Collector Serviceが止まっている場合があるかもしれない。
 長く使っている開発環境で変な操作が介入してしまうと知らないうちに起きる問題なので、非常に厄介だ。
 今日はVS2022に関わる諸問題を解決するだけで随分時間を使ってしまった。
 嫌なことである。

nvToolsExt64_1.libがロードできないと怒られる場合

 Visual C++ 2022でlibtorchプロジェクトスケルトンをビルドした処、nvToolsExt64_1.libがロードできないと怒られた。
 ライブラリは、libtorch2.2.1+CUDA 12.1+cuDNN8.9.7
 どうやらCUDA 12.1にはCUDA 18にあったNsight NVTXインストールオプションが無いようで、CUDA 18インストーラーからNsight NVTXインストールオプションだけを有効にしてインストールすると、C:\Program Files\NVIDIA Corporation\NvToolsExt\lib\x64、に上記libファイルがインストールされ解決する。
github.com
 nvidia側が、元々Nsight NVTXが入っている環境でインストーラーを作ってしまい、12.1に追加し忘れたかのような挙動だ・・・
 地味に時間を取られた問題だ。

DLLにMQL5のdatetime型を引数として渡す時

 MQL5のdatetime型はMQL5内ではlongと互換性があり64bit(8バイト)である。
 64bitのC++プログラムに渡す場合、longが32bitでlong longが64bitの場合には、DLL側の引数の型をlong long型にする。
 C++DLL側の引数を32bitのlong型にしてしまうと、ポインターから要素番号でアクセスした時に意図しない結果を返す。
 VC++2022の64bitプログラムで試したところ、longポインターとしてdatetime配列を受けた場合、その引数に要素番号で順次アクセスすると、0を挟んで一つ飛ばしにdatetimeの値を取るという挙動になった。

  • 0,datetime,0,datetime,0,datetime,0,datetime,0,datetime・・・
__declspec(dllexport) int send_datetime(
	long long* _datetime
	, int _sz
)
{
    // do something
}

 一方、MQL5でDLLを#importする際に宣言する関数型は、

int send_datetime(
  long &[], int _sz
);

などとする。longはdatetimeでも良いだろう。どちらもMQL5内では64bitである。

MetaEditor 5.00 Build 4153

 MetaEditorIDEによるデバッグ機能がかなり向上している。

  • MetaEditorのメニューからヒストリーとリアルデータを選び簡単にデバッグ実行が出来る
  • エキスパートアドバイザーをデバッグするときに使用しているインディケーターを一緒に表示してくれる

 
 これらは過去のバージョンでは非常にやりづらいものだった。
 ちゃんと改善していて偉い。