C#とPythonが出会う機械学習インターロップの概念図

C#からHugging Faceモデルを呼ぶ:DotNetPy 0.6.0でWhisper・sentence-transformers・Stable Diffusionを動かす

週末に小さなC#ライブラリ DotNetPy の0.6.0をリリースしました。CPythonのC APIを直接呼び出して、.NETアプリの中でPythonを動かすインターロップライブラリです。本記事は0.6.0に含まれる3つの機械学習サンプル(sentence-transformersによる意味検索、Whisperによる音声認識、Stable Diffusion Turboによる画像生成)をどうまとめたか、そしてその過程でPEP 703 free-threaded CPythonをどのように検証したかを記録したものです。 出発点:手元にあるのはC#だけ、モデルはHugging Faceにある 数か月ごとに同じパターンが繰り返されます。字幕用にWhisperが必要、検索用にsentence-transformerが必要、ときにはStable Diffusionのようなモデルを使いたい。しかし手元にあるのはC#一つだけ。こんなとき、よく取られる回避策にはそれぞれ決定的な欠点があります。 ONNXに変換する。 画像系やエンコーダ系のモデルにはよく合いますが、新しいアーキテクチャやdiffusionパイプラインでは、変換そのものが別プロジェクトになります。 Pythonマイクロサービスとして立ち上げる。 プロセスが2つ、デプロイのシナリオが2つ、そしてホットパスにネットワークホップが1つ追加されます。 外部APIを呼び出す。 コストがかかり、インターネット接続が必要で、データが箱の外に出ます。 pythonnet や CSnakes を使う。 堅実な選択肢ですが、pythonnetはまだNative AOTをサポートしておらず、CSnakesはSource Generatorベースのワークフローを強制します。さらに、両ライブラリともfree-threaded CPythonビルドに対する公開された検証結果をまだ出していません。 私はもう少し薄い選択肢が欲しかったのです。C#コードの中にPythonスニペットを文字列としてインラインで書き、配列をそのまま渡し、JSON形式で結果を受け取り、全体がAOTコンパイルされて単一バイナリになる 形です。それがDotNetPyの目標であり、以下の3つのサンプルはすべてGPUのない普通のWindows 11ノートPCで最初から最後まで動作します。 サンプル1 — sentence-transformers による意味検索 最初のサンプルは小さなコーパスを埋め込み、クエリをエンコードして、最も類似する上位K件の文を返します。戻り値はDotNetPyValueで、内部的にはJSONドキュメントをラップしたもので、GetString()・GetInt32()・GetDouble()、そしてパスベースのプロパティアクセスを通じて.NETの世界に戻ってきます。 using DotNetPy; using DotNetPy.Uv; using var project = PythonProject.CreateBuilder() .WithProjectName("dotnetpy-ml-embeddings") .WithPythonVersion("==3.12.*") .AddDependencies( "sentence-transformers==2.7.0", "transformers==4.40.2", "torch>=2.2,<2.5") .Build(); await project.InitializeAsync(); var executor = project.GetExecutor(); executor.Execute(@" import numpy as np from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2') "); var corpus = new[] { "Python is a popular programming language for data science.", "C# and .NET are great for building enterprise applications.", "Rust offers memory safety without garbage collection.", "Pizza is delicious with various toppings.", // … }; var query = "Tell me about programming languages"; using var hits = executor.ExecuteAndCapture(@" corpus_emb = model.encode(corpus, normalize_embeddings=True) query_emb = model.encode([query], normalize_embeddings=True)[0] sims = corpus_emb @ query_emb top_idx = np.argsort(-sims)[:3] result = [ {'rank': int(rank + 1), 'score': float(sims[i]), 'text': corpus[int(i)]} for rank, i in enumerate(top_idx) ] ", new Dictionary<string, object?> { { "corpus", corpus }, { "query", query } }); foreach (var hit in hits!.RootElement.EnumerateArray()) { Console.WriteLine($" {hit.GetProperty("rank").GetInt32()}. " + $"[{hit.GetProperty("score").GetDouble():F3}] " + $"{hit.GetProperty("text").GetString()}"); } 実際の出力はこうなります。 ...

2026年5月11日 · 4 分 ·  rkttu