BaseModelのkeyの命名規則を変換するAliasGeneratorについてメモ【備忘録】

お疲れ様です。

PydanticのBaseModelには要素のキー名の命名規則を変換する機能があります。 AliasGeneratorという機能で、BaseModelを定義する際に設定をすることができます。

pydantic.dev

例えば、frontendでTypeScript、backendでPythonを使用して開発しているときなどプログラム言語による変数の命名規則が異なるときに、API通信のタイミングで自動で言語ごとの命名規則の形式に変換することができます。

命名規則については以下参考。 一般的にPythonではsnake_case、TypeScriptではcamelCaseが変数の命名規則として使用されます。

qiita.com


設定方法は下記のようにBaseModelのmodel_configにalias_generatorを設定する形です。 今回はsnake_caseをcamelCaseに変換したいのでto_camelを設定しています。

pydantic.alias_generatorsからはto_camelの他、to_snakeとto_pascalをimportして利用可能です。 また、カスタムで作成した関数を設定することも可能です。

model_configでAliasGeneratorだけ設定したBaseModelを作成しておき、新たに作成するBaseModelに継承すると便利です。

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
    
    
class CamelModel(BaseModel):
    """snake_case <-> camelCase の相互変換を設定した継承用BaseModel"""
    
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,
    )

class RequestModel(CamelModel):
    """リクエスト用のBaseModel"""
    user_id: int = 1234
    user_name: str = "test_user"
    

class ResponseModel(CamelModel):
    """レスポンス用のBaseModel"""
    user_id: int
    user_name: str
    message: str

上記でBaseModelを使用してFastAPIでAPIエンドポイントを作成してみました。

@app.post("/test")
def base_model_test_1(
    payload: RequestModel
) -> ResponseModel:
    """postメソッドでテスト"""
    # 受け取ったリクエストモデルを表示
    print("変換なし:", payload.model_dump())
    print("変換あり:", payload.model_dump(by_alias=True))
    
    # レスポンスモデルを返す
    response = ResponseModel(
        user_id=payload.user_id,
        user_name=payload.user_name,
        message="postメソッドでレスポンスモデルのテスト"
    )
    
    return response

実際に実行し、SwaggerUIを確認してみるとこんな感じで表示されます。 リクエストとレスポンスのschemaがcamelCaseで表示されており、リクエストの値の入力時もcamelCaseのまま行います。

swaggerui

以下はAPI内の処理で受け取った値をprintしてみた表示結果です。 受け取った値を辞書型にdumpするとsnake_caseの形で表示されていますね。 このようにPythonの処理内ではsnake_caseで扱うことができます。

一応aliasを適用するとcamelCaseに変換することも可能です。あまり使用することはないと思いますが…。

print

ソースコード

今回の検証用に作成したコードはこちらに残しています。

github.com

LLM-jp-4を動かしたりRAGを試したりした記録

お疲れ様です。

Gemma4と同時期くらいに日本語特化のLLMであるLLM-jp-4が出ていました。 こちらも試してみたのでそれをまとめます。

LLM-jp-4について

国立情報学研究所のLLM研究のグループが開発した新たな国産LLMです。 日本語性能でGPT-4oを上回る性能を出したとのこと。
ライセンスは「Apache-2.0」なので利用もしやすいですね。 今後このモデルをベースに新たな日本語LLMが出てくるのも期待できそうです。

www.nii.ac.jp

実装

モデルはHuggingFaceで公開されているのみなので、今回はこちらを使用します。 Ollamaで使えれば楽なのですがこればかりは仕方ないですね…。

huggingface.co

ソースコード

ソースコードはいつもOllamaのモデルの検証に使用しているリポジトリを使用しました。 こちらにHuggingFaceのモデル対応の処理を新たに作成しています。

github.com

とりあえず動かしてみる

まずはLLM-jp-4のモデルをそのまま動かしてみました。 モデルページのUsageの部分のコードをベースにStreamlitのチャットUIで動くように改修しました。 公式が公開しているcookbookにも同様にサンプルコードがあるようです。

素の状態の出力だと以下のように「<|channel|>」のような区切りトークンを含む出力になります。

output

区切りトークンを含む生成文をパースしてモデルの生成文のみを抜き出すようにそれ用の関数を作成して対応しました。

output_parsed

コード

def response_generator_huggingface_model() -> str:
    """HuggingFaceのモデルを使用
    """
    tokenizer = AutoTokenizer.from_pretrained(
        HF_MODEL,
        trust_remote_code=True,
    )
    model = AutoModelForCausalLM.from_pretrained(
        HF_MODEL,
        dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True,
    )
    model.eval()
    
    system_prompt = [{
        "role": "system",
        "content": "あなたはユーザの質問に答えるアシスタントです。回答は200文字程度で要点だけをまとめて簡潔に答えてください。"
    }]
    
    prompt: str = tokenizer.apply_chat_template(
        system_prompt + st.session_state.messages,
        tokenize=False,
        add_generation_prompt=True,
        reasoning_effort="low",  # {"low", "medium", "high"}
    )
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    
    with torch.no_grad():
        output_tensor = model.generate(
            **inputs,
            max_new_tokens=256,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
        )
    
    # 出力データを変換して返答文を取得
    generated_ids: list[int] = output_tensor[0][inputs["input_ids"].shape[1]:].tolist()
    response = tokenizer.decode(generated_ids)
    parsed_response = parse_chat_output(response)
    
    response_html = Markdown().convert(parsed_response["assistant"]["message"])
    
    return response_html

RAGを試してみる

次にいつものようにRAGも試してみました。

今回はHuggingFaceのモデルしかないので、最初はLangChainのHuggingFacePipelineを使って実装していました。ただこれがうまくいかず…。
以下にスクショを載せますが生成された返答文に与えたコンテキストがすべて残っておりめちゃくちゃ長くなっています。 加えて先ほど素の状態で動かしたときのような区切りトークンになっておらず、出力のたびに形式が異なるので対応が難しい状態でした…。

rag_output_1

コード(改良前)

def response_generator_langchain_huggingface_rag() -> str:
    """langchain_huggingfaceを使用+RAG
    """
    tokenizer = AutoTokenizer.from_pretrained(
        HF_MODEL,
        # trust_remote_code is required to load custom tokenizer and reasoning parser.
        trust_remote_code=True,
    )
    model = AutoModelForCausalLM.from_pretrained(
        HF_MODEL,
        dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True,
    )
    pipe = pipeline(
        "text-generation", model=model, tokenizer=tokenizer, 
        max_new_tokens=256, do_sample=True, temperature=0.7, top_p=0.9
    )
    llm = HuggingFacePipeline(pipeline=pipe)
    
    # 直前のユーザの入力を取得
    user_input = st.session_state.messages[-1]["content"]
    
    # ベクトル化する準備
    model_kwargs = {
        "device": "cpu", # NOTE: モデルと合わせてVRAM容量を超えるのでCPUで実行
        "trust_remote_code": True
    }
    embedding = HuggingFaceEmbeddings(
        model_name="pfnet/plamo-embedding-1b",
        model_kwargs=model_kwargs
    )
    
    # DBを読み込んで知識データ取得
    vectorstore = Chroma(collection_name="elephants", 
                         persist_directory=DATABASE_DIR, 
                         embedding_function=embedding)
    docs = vectorstore.similarity_search(query=user_input, k=10)
    context = "\n".join([f"Content:\n{doc.page_content}" for doc in docs])
    
    messages = [
        ROLES[msg["role"]](content=msg["content"]) 
        for msg in st.session_state.messages[:-1]
    ] + [HumanMessage(content=RAG_PROMPT.format(question=user_input, context=context))]
    
    response = llm.invoke(messages)
    
    response_html = Markdown().convert(response)
    
    return response_html

tokenizerのchat_templateを使うと良いとの情報を得たので、LangChainを使わず素の状態のモデルを動かしたときのコードをベースにRAG用コードに改修して改良版を作成してみました。 そうすることで、出力の形式が統一され生成文をパースできるようになりました。
以下のように返答文の部分だけうまく取り出せています。システムプロンプトも加えているので生成文の形式が少し変わっています。

rag_output_2

生成にかかる時間も大体1分ちょっとくらいです。 前回の記事で比較したgpt-oss:20bやgemma4:e4bと一応変わらないくらいの処理時間ではありますね。

rag_erapsed_time_2

コード(改良後)

def response_generator_langchain_huggingface_rag() -> str:
    """langchain_huggingfaceを使用+RAG
    """
    tokenizer = AutoTokenizer.from_pretrained(
        HF_MODEL,
        # trust_remote_code is required to load custom tokenizer and reasoning parser.
        trust_remote_code=True,
    )
    model = AutoModelForCausalLM.from_pretrained(
        HF_MODEL,
        dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True,
    )
    model.eval()
    
    # 直前のユーザの入力を取得
    user_input = st.session_state.messages[-1]["content"]
    
    # ベクトル化する準備
    model_kwargs = {
        "device": "cpu", # NOTE: モデルと合わせてVRAM容量を超えるのでCPUで実行
        "trust_remote_code": True
    }
    embedding = HuggingFaceEmbeddings(
        model_name="pfnet/plamo-embedding-1b",
        model_kwargs=model_kwargs
    )
    
    # DBを読み込んで知識データ取得
    vectorstore = Chroma(collection_name="elephants", 
                         persist_directory=DATABASE_DIR, 
                         embedding_function=embedding)
    docs = vectorstore.similarity_search(query=user_input, k=10)
    context = "\n".join([f"Content:\n{doc.page_content}" for doc in docs])
    
    # システムプロンプトの用意
    system_prompt = [{
        "role": "system",
        "content": "あなたはユーザの質問に答えるアシスタントです。回答は最大500文字でまとめて簡潔に答えてください。"
    }]
    
    # 直前のユーザの入力を取得
    rag_input = [{
        "role": "user",
        "content": RAG_PROMPT.format(question=user_input, context=context)
    }]
    
    prompt: str = tokenizer.apply_chat_template(
        system_prompt + st.session_state.messages[:-1] + rag_input,
        tokenize=False,
        add_generation_prompt=True,
        reasoning_effort="low",  # {"low", "medium", "high"}
    )
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    
    with torch.no_grad():
        output_tensor = model.generate(
            **inputs,
            max_new_tokens=512,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
        )
    
    # 出力データを変換して返答文を取得
    generated_ids: list[int] = output_tensor[0][inputs["input_ids"].shape[1]:].tolist()
    response = tokenizer.decode(generated_ids)
    parsed_response = parse_rag_output(response)
    
    response_html = Markdown().convert(parsed_response)
    
    return response_html

gemma4:e4bでRAGを試す【Ollama+LangChain】

お疲れ様です。

GoogleからGemma4がリリースされたので、今回はこちらを試してみたいと思います。

blog.google

gpt-ossを使用して作成したRAGのコードを使用します。 Gemma4はOllamaで簡単に使用することができる のでかなり手が出しやすいです。爆速で対応してくれたOllamaにも感謝。 Ollamaを最新版にしないとモデルのダウンロードができないようなので、最新版に更新しておきましょう。

  • 以前の記事(併せてどうぞ)

fallpoke-tech.hatenadiary.jp

  • ソースコード

github.com

Gemma4の導入

まずはOllamaでGemma4のモデルをダウンロードします。 Gemma4は4種のモデルがありますが実行環境のGPUがRTX4060tiのVRAM16GBなので、それで動作するモデルサイズの"gemma4:e4b"を指定しました。

ollama pull gemma4:e4b

download

インストールが完了したら、一覧を確認。gpt-oss:20bと比べるとモデルサイズが小さいです。

ollama list

list

RAGを実行

早速RAGを実行してみました。 ソースコードの方はモデル名の指定だけ変更すればすぐに使用できます。

gpt-oss:20bの方も使用可能なので、そちらと比較してみます。

出力結果

RAGに使用したデータとプロンプトは同じ内容です。
コンテキストの内容が同じなので生成の内容自体は同じような感じですが、情報のまとめ方が異なります。

  • gemma4:e4b

rag_gemma4

  • opt-oss:20b

rag_gpt-oss

実行時間

それぞれのモデルでの返答生成の実行時間を計測してみました。3ターン程チャット形式で質問してみました。
gemma4:e4bの方がモデルサイズが小さいのもあるのか、返答までの時間が速いですね。 出力結果は大差ないので、gemma4:e4bでも十分な気がします。

  • gemma4:e4b

time_gemma4

  • opt-oss:20b

time_gpt-oss

設定されたloggerのログレベルを個別に設定する方法【備忘録】

お疲れ様です。

Pythonの標準モジュールのloggingを使ってログを表示する際、インストールした他のライブラリに設定されたログが邪魔になることがありました。
そういう時にライブラリごとに個別にログレベルを設定して表示を制限する方法があったのでそれをメモ。 開発版と本番でプログラムのログレベルの設定を切り替えたりする際にも使えると思います。

ロガーを含むライブラリ例

ライブラリごとに個別にログレベルを設定する方法

プログラム全体のログレベルはDEBUGで特定のライブラリのログだけWARNINGに設定した例です。
こうすることにより、自分が設定したログはDEBUG以上のログが表示され、ライブラリのログはWARNING以上でないと表示されないようにできます。

import logging

# 全体のログレベル
logging.basicConfig(level=logging.DEBUG)

# 特定のライブラリだけ指定してログを抑制
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("boto3").setLevel(logging.WARNING)
logging.getLogger("pymongo").setLevel(logging.WARNING)

ライブラリ内で名前を分けてさらに細かく設定されている場合にも対応可能です。
以下の例なら、pymongoのログのうちコマンド操作をログのみログレベルを変更することができます。 loggerに設定されたnameを指定する形です。

# pymongoの"command"のログだけ制御
logging.getLogger("pymongo.command").setLevel(logging.WARNING)

pymongoのドキュメントにはロガーの設定についての記載もありました。 pymongoは特にDEBUGのログが大量に表示されて自分が見たいログがすぐに流れるので、全体のログレベルを下げる場合は個別設定で制御した方が良いですね…。

www.mongodb.com

Windows環境のPythonでデフォルトの文字コードをUTF-8にする設定【備忘録】

お疲れ様です。

Windows環境ではデフォルトの文字コードのデフォルトがshift-jis(cp932)になります。
これが原因でエラーが起こることもあり、対処方法の1つとしてデフォルトの文字コードをutf-8に変更する方法を調べたのでメモを残します。 一部のライブラリだと内部でテキストベースのファイルを読み込むことがあるので、その際にエラーが起こるということがたまにあります。 私はpycocotoolsのjsonファイルの読み込みでこの問題にぶち当たりました…。

以下のサイトが参考になります。(というか知りたかったことがすべて書かれていました。) qiita.com

結論、デフォルトの文字コードをutf-8に変更するためには環境変数でPYTHONUTF8=1を設定することです。
Windows環境ならターミナル上でsetコマンドで設定するのが簡単かなと思います。 恒常的にデフォルトをutf-8にしたい場合はコントロールパネルの「環境変数を編集」で追加しておくのでも良いです。

set PYTHONUTF8=1


注意点として、こちらの設定はPythonプログラムの実行前に変更しないと反映されません。
例えば、下記のようにプログラム内でPYTHONUTF8を追加や編集した場合はデフォルトがshift-jisのままになります。

import os

os.environ["PYTHONUTF8"] = "1"

また今回はWindows環境なのでLinux環境のように実行時のコマンドで特定の環境変数を指定することはできません。

PYTHONUTF8=1 python program.py


備忘録としては以上です。
挙動確認に使用したプログラムはGitHubにも残しておきますので、実際に挙動を見てみたい場合はどうぞ。

github.com

画像分類モデルの特徴量を可視化してみる【備忘録】

お疲れ様です。

今回は画像分類モデルが分類の判断に使用する特徴量を可視化してみる回です。 モデルがどのように画像の特徴をとらえているかをおおまかに知ることができ、例えばデータセットの見直しなどでモデルの精度改善の検討ができるようになるかと思います。

ソースコード

例によってソースコードはこちらの画像分類モデルをまとめたリポジトリに残しています。 使用するだけなら学習済みモデルを用意し、main_inference_feature.pyを実行してもらえれば使用できます。

github.com

処理内容

処理の流れとしては以下のような感じです。

  1. データセットを使用してモデルを作成
  2. 推論を実行しその際の特徴量を取得
  3. 得られた特徴量を次元削減
  4. 次元削減後の特徴量を可視化

1. データセットを使用してモデルを作成

モデル作成は普通の学習プログラムを使用します。
今回は以前作成したSEResNeXt50の学習済みモデルを対象とします。 推論で使用するデータセットもこの時のデータセットと同じものです。

fallpoke-tech.hatenadiary.jp

2. 推論を実行しその際の特徴量を取得

学習済みモデルを使用して推論を実行します。 この時モデルから取得するのはモデルの最終出力層(画像分類モデルなら基本はLinear層)を通したスコアではなく、その直前の状態の特徴ベクトルです。

リポジトリではtimmライブラリを使用していますが、このライブラリではモデルのインスタンスに特徴ベクトルを取得するメソッドがあります。 こちらを利用してデータセットの画像1枚ごとの特徴量を取得します。

def feature_extraction(
    self,
    input_img: np.ndarray,
) -> np.ndarray:
    """1画像で特徴量抽出の処理を実行
    """
    input_img = input_img.to(self.device)
    input_img = input_img.unsqueeze(0)
    
    with torch.no_grad():
        output = self.model.forward_features(input_img)
    
    return output.detach().cpu().numpy()[0]

出力(変数output)の形状を確認すると"[1, 2048, 7, 7]"となっています。 頭の"1"はバッチサイズなので無視するとして、SEResNeXtでは画像1枚で"2048x7x7"次元の特徴ベクトルが取得できることがわかります。
shape

3. 得られた特徴量を次元削減

すべての画像の特徴ベクトルを取得したら、次は次元削減を適用します。
次元削減手法はUMAPを使用します。

  • UMAPについて(from ChatGPT)

    UMAP(Uniform Manifold Approximation and Projection)は、高次元データを2次元や3次元に圧縮して可視化する次元削減手法です。データ同士の近さ(局所構造)を保つことを重視しつつ、全体の配置関係もある程度維持します。t-SNEに比べて高速で、大規模データにも適しており、新しいデータを既存の低次元空間に写像できる点が特徴です。主に特徴量や埋め込みの可視化に用いられます。

PythonでUMAPを使用する方法について詳しくは以下を参照ください。

boritaso-blog.com

作成したプログラムでは以下のように使用しています。
画像データすべての特徴量を1つにまとめてUMAPのインスタンスに与えています。 インスタンス作成時に指定した"n_components"が次元削減後の次元数です。 今回は3Dプロットで可視化したいので各画像データの特徴ベクトルを3次元まで削減するように設定しています。

# UMAPインスタンスの作成
reducer = umap.UMAP(n_components=3)

# 画像を1枚ずつ特徴量抽出
features = []
labels = []
for i in tqdm(range(len(test_dataset)), desc="inference"):
    input_img, lbl = test_dataset[i]
    feat = infer.feature_extraction(input_img)
    features.append(feat.flatten())
    labels.append(lbl)

# UMAPによる次元削減
embeddings = reducer.fit_transform(np.array(features))

4. 次元削減後の特徴量を可視化

ここまでで作成したデータを実際に可視化します。
可視化ライブラリにはplotlyを採用しました。 今回は3Dプロットで可視化したいのでplotlyで作成した方が動作が軽かったです。 matplotlibでも作成自体は可能ですが、かなり動作が重かったです…。

コードはざっくりとこんな感じです。下記以外に細かく設定をしているので詳細はリポジトリの"modules/visualize_features.py"を参照してください。

# 3次元散布図の作成
fig = scatter_3d(
    x=features[:, 0],
    y=features[:, 1],
    z=features[:, 2],
    color=[f"{classes.index(classes[label])}-{classes[label]}" for label in labels],
    color_discrete_sequence=color_map,
    labels={"color": "Classes"},
)

結果の確認

1~4を実行した結果が以下のようになります。 ブラウザ上で表示され、マウス操作で拡大や軸を回転させることができます。

result

このように右の判例から表示するデータを絞ることもできます。 「bell pepper(ピーマン)」と「capsicum(唐辛子)」に絞っていますが、これらは近い3次元空間上で近いところに位置しています。 少し見にくいかもしれませんが、混同行列を見てもラベル3(bell pepper)とラベル5(capsicum)の間で誤分類しているのでしっかりとモデルが得た特徴量の傾向を可視化できていそうです。

result2

confusion_matrix

画像分類モデルSEResNeXtについて調べたまとめ

お疲れ様です。

画像分類モデルのSEResNeXtについてのメモです。
個人的にCNNベースの画像分類のモデルアーキテクチャとしてはEfficientNetV2と並んでよく使います。

論文

SEResNeXtの重要なアーキテクチャであるSEブロックに関する論文です。
後の概要にも記載がありますがSEブロックと画像分類モデルResNeXtを組み合わせたものがSEResNeXtと呼ばれています。

arxiv.org

概要(from ChatGPT)

SEResNeXt(Squeeze-and-Excitation ResNeXt)は、 ResNeXtSE(Squeeze-and-Excitation)ブロックを組み込んだ画像分類モデルです。 ResNet / ResNeXt / SENet の長所を組み合わせた構造になっています。


1. ベースとなる3つの考え方

① ResNet(残差学習)

  • skip connection(残差接続) により勾配消失を防ぐ
  • 非常に深いネットワークでも学習が安定
y = F(x) + x

② ResNeXt(Cardinality)

ResNetを拡張し、並列な畳み込みの分岐数(cardinality)を増やす設計。

  • チャンネル数や層を増やすより 効率的に表現力を向上
  • 32x4d のような表記が特徴

例:

  • 32x4d

    • cardinality = 32(分岐数)
    • 各分岐のチャネル幅 = 4

③ SENet(チャンネル注意機構)

重要なチャネルを自動で強調する仕組み。

SEブロックの流れ

  1. Squeeze

    • Global Average Pooling
  2. Excitation

    • 全結合層でチャネルごとの重みを計算
  3. Scale

    • 元の特徴マップに重みを掛ける
channel-wise attention

2. SEResNeXtの構造

ResNeXtの各Residual BlockにSEブロックを追加

Input
 ↓
Grouped Convolution (ResNeXt)
 ↓
SE Block(チャネル注意)
 ↓
Residual Add

特徴

  • ResNeXtの 高い表現力
  • SENetの 重要特徴の強調
  • パラメータ増加は比較的少ない

3. モデル名の読み方

例:seresnext50_32x4d

要素 意味
50 ネットワークの深さ
32 cardinality(分岐数)
4d 各分岐のチャネル幅
se Squeeze-and-Excitation

4. メリット・デメリット

✅ メリット

  • 高精度(ImageNetで実績あり)
  • ResNetより 効率的
  • 少量のパラメータ増加で性能向上
  • 転移学習に強い

❌ デメリット

  • ResNetより計算量がやや増加
  • 軽量モデル(MobileNet系)ほど速くない

5. どんなタスクに向いているか

  • 一般的な 画像分類
  • 医用画像・工業検査
  • 少〜中規模データセットの転移学習
  • 高精度が求められるタスク

OSSライセンスはtimmから利用すると基本的にはApache-2.0なので、商用利用も問題なさそうです。
timm以外から利用する場合は要確認です。 (例えば、ResNeXtの公式実装ではBSDライセンスになっています。)

実装

SEResNeXt自体の公式実装は探してみた感じはなさそうだったので、実際にモデルを使用したい場合はtimmライブラリから使用することになると思います。

モデルはHuggingFaceのモデルページで探すことができます。
.racm_in1k.gluon_in1kの2パターンありますが、.racm_in1kの方を使うのが無難のようです。

huggingface.co

お試し

普段使用している画像分類モデルのリポジトリで使用できるように実装しました。

github.com

コンフィグファイル(config/train_config.toml)のmodel_nameに"SEResNeXt"を指定すると使用できます。

model_name = "SEResNeXt"

上記のコードを使って実際に動かしてみました。
EfficientNetV2とVisionTransformerでも同じ設定で実行し、結果を比較してみます。

モデルはそれぞれtimmから利用しています。

データセットについては、過去にも使用したFood_and_Vegetablesを使用しています。 huggingface.co

実際の結果を見てみます。上記のデータセットで試した結果なので参考程度に。

学習曲線が以下です。 20epochまでで比較しましたが、SEResNeXtだけまだ収束していない感じですね…。

learning_curve

SEResNeXtのみ追加で50epochまで学習を回してみましたが、最終的なベストの精度は同じくらい(97%程)になりました。

seresnext_learning_curve

また、全体の実行時間とGPUメモリの使用量を計測した結果が以下になります。
学習の収束が遅いですが、モデル自体が軽いので同条件だと実行時間は早く、GPUメモリも余裕があります。 バッチサイズを上げる余裕があるので更なる高速化もできそうです。

実行時間(s) GPUメモリ使用量(GB)
SEResNeXt 4006.2 6.379
EfficientNetV2 4216.3 13.692
VisionTransformer 4285.8 6.421