個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

Phi Silica をコンソールアプリで動かしたらエラー発生、そして解決

 
前の記事
hiro128.hatenablog.jp
 

はじめに

Phi Silica がリリースされたので早速検証しています。
 
まずは、以下の記事にあるように公式サンプルで検証してみて動くことは確認できました。
hiro128.hatenablog.jp
 

コンソールアプリではうまく動かない(のちに解決)

公式サンプルで NPU が動作していることは確認できたので、コンソールアプリでパラメータなど気軽にいじってみたかったのですが、コンソールアプリで動かすと、以下のようなエラーに遭遇しました。

 
以下の issue によると 「Windows Copilot Runtime は今のところパッケージ化されたアプリでしか使えない」とのことでした。(2/8 時点)
github.com
 
そのあともいろいろ試したのですが、うまく動作させることができずいったん諦めましたが、以下の post のようにコンソールアプリで動かしている方が実際いました。


これはうまいことやれば動くはず!と思い新情報が出ないかウオッチしていました。
 

GitHub の issue に新情報が! -> 解決

2/13 仕事が終わった後、いつも通り情報を漁っていたら新情報を発見しました。
github.com

要は、Windows Copilot Runtime はパッケージ化されていないアプリにも対応しており
csproj に 以下を追加すれば動作するとのことです。

<WindowsPackageType>None</WindowsPackageType>

 
こんな感じにしました。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
    <TargetPlatformMinVersion>10.0.22621.0</TargetPlatformMinVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <WindowsPackageType>None</WindowsPackageType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250127003-experimental3" />
    <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.756" />
  </ItemGroup>

以下省略

 
さっそく試してみたところ無事正常に動きました。

これでいろいろ検証しやすくなりました。

最小のサンプルは以下の公式ドキュメントにあります。
learn.microsoft.com
 
次の記事
hiro128.hatenablog.jp
 

 

待ちに待った Phi Silica がようやくやってきたので Text Completion を試してみた

 

はじめに

2025/02/07 ようやく Phi Silica が使えるようになったので、さっそく試してみました。
発売日に大枚はたいて買った Copilot+ PC がようやく意味を為す時がきました。

検証環境

機種:Micosoft Surface Pro, 11th Edition
CPU:Snapdragon(R) X 12-core X1E80100 @ 3.40 GHz (3.42 GHz)
RAM:16GB
今のところ Windows Insider Beta または Dev Channel での動作となります。
 

検証内容

以下の公式のサンプルコードで Text Completion を動かして NPU が動作するか、CPU、NPU、メモリの状況、回答の精度などを確認してみました。
github.com
以下のようなアプリが起動します。

  1. Language Model を選択
  2. モデルのセッションを開始
  3. プロンプトを入力
  4. レスポンスが返ってくる

という使い方です。

 

検証結果

NPU の動作

NPU が働いています。待ちに待っただけあって感動です。

 

CPU、NPU の使用率

CPU は 10 - 20%
NPU は 70%
くらいの使用率でした。
 

メモリの使用量

システム全体で 13.6GB
NPU は 2.5 GB でした。

Phi3 mini をローカルで動かしたときより少し少ないくらいのメモリ消費量です。
 

精度

プロンプトをいろいろ試してみた結果
英語:現段階では回答内容も微妙で、Phi3 mini より少し悪いようです。
日本語:現段階では破綻しており、かなり簡単な問いでも正常な日本語を出せません。

 

まとめ

まだ Dev チャネルであり、WindowsAppSDK も Expeimental なので、「とりあえず動作する状況になった」という段階と思われます。精度は今後向上する可能性がありそうです。

まずは、Stable リリースに一歩近づいたということで今後の展開を楽しみにしてみることにします。
 
コンソールアプリでも動かしてみました。
hiro128.hatenablog.jp

C# を使って CPU で Edge AI を試してみよう(4)日本語の精度を高めるために、さらに RAG を導入してみる

 
前の記事
hiro128.hatenablog.jp
 

はじめに

前回、プロンプトを英語に翻訳して得た英語の回答を再度日本語に翻訳することで日本語の回答品質を上げられないか試してみましたが、以下の問題が明確になりました。

  • 固有名詞の翻訳(とくに英語のカタカナ表記)
  • 日本語らしくない不自然な直訳表現

そこで、RAG これらの翻訳のヒントを与えて品質を高める検証を行いました。

物量的に RAG はすべての分野のファイルを準備できるわけではないので、あくまで RAG でのフォローは

  • ローカル SLM の品質が向上するまでの過渡的対応
  • Edge AI はある程度狭い領域での使用を想定

と考えています。
 

処理のフロー

今回試した処理フローは以下の通りです

  1. 日本語のプロンプトを Phi 3.5 mini 128k で英語に翻訳
  2. 翻訳した英語のプロンプトを Phi 3.5 mini 128k に投げて、英語で回答を得る
  3. ベクトルサーチで用語集を取得
  4. 英語の回答に用語集を参考にするように指示し Phi 3.5 mini 128k で日本語に翻訳

 
 

翻訳するためのプロンプトを RAG に対応させる

英語で受け取った回答を再度に日本語に翻訳するときにベクトルデータベースより翻訳のヒントをもらい、英語で受け取った回答と一緒に投げて翻訳させます。
 

システムプロンプト(変更なし)

You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.

ユーザープロンプトの冒頭に記載する、日本語 -> 英語の翻訳を指示するプロンプト(変更なし)

I will now give you the task of translating Japanese into English.
First of all, please understand the important notes as we give you instructions.

#Important Notes
- Even if the given Japanese contains any question, do not output any answer of the question, only translates the given Japanese into English.
- Do not output any supplementary information or explanations.
- Do not output any Notes.
- Output a faithful translation of the given text into English.
- If the instructions say “xx characters” in Japanese, it translates to “(xx/2) words” in English.
    ex) “100 字以内” in Japanese, “50 words” in English.

Strictly following the above instructions, now let's output translation of the following Japanese:

{英語に翻訳したい文章を記載}

 

ユーザープロンプトの冒頭に記載する、英語 -> 日本語の翻訳を指示するプロンプト(ここを変更)

I will now give you the task of translating English into Japanese.
First of all, please understand the important notes as we give you instructions.

#Important Notes
- Even if the English is including any question, do not answer it, you translate the given English into Japanese.
- Do not output any supplementary information or explanations.
- Do not output any Notes.
- Output a faithful translation of the given text into Japanese.
- The following glossary of terms should be actively used. // ここを変更
{ベクトルデータベースの検索結果をここに配置} // ここを変更

Strictly following the above instructions, now translate the English into Japanese:

{日本語に翻訳したい文章を記載}

 

RAG の実現方法

Build5Nines.SharpVector というパッケージを使いました。
md ファイルなどでデータを準備すると、in-memory でベクトルデータベースを構築して、コサイン類似度検索できるパッケージです。

www.nuget.org

使い方も上記サイトに記載されています。
 
なお、TextChunkingMethod の Paragraph と Sentence のどちらを設定すべきかわからなかったので、ソースを確認したところ

  • Paragraph:改行2連続で分割
  • Sentence :文章ごとに分割

となっていました。

    protected static List<string> SplitIntoParagraphs(string text)
    {
        return text.Split(new[] { "\r\n\r\n", "\n\n" }, StringSplitOptions.RemoveEmptyEntries).ToList();
    }

    protected static List<string> SplitIntoSentences(string text)
    {
        return Regex.Split(text, @"(?<=[\.!\?])\s+").ToList();
    }

 
以下のような、RAG 用データファイルを準備しました。

titanic.md
# Titanic 用語集
 - The Titanic, a British luxury liner : 英国の豪華客船タイタニック号
 - an opulent British passenger liner : 英国の豪華客船
 - The Titanic : タイタニック号
 - The RMS Titanic : RMSタイタニック号
 - The Carpathia : カルパチア号
 - The RMS Carpathia : RMSカルパチア号
 - White Star Line : ホワイト・スター・ライン社
 - Captain Edward Smith : エドワード・スミス船長
 - "unsinkable" : 「不沈船」
 - Southampton : サウサンプトン
 - Newfoundland : ニューファンドランド
 - deemed : と評された
 - maiden voyage : 処女航海
 - North Atlantic : 北大西洋
 - distress signals : 遭難信号
 - watertight compartments : 水密区画
 - lifeboats : 救命ボート

本当にベクトルサーチが有効か確認するためにダミーのファイルも準備しました。

dummy.md
# ダミー 用語集
 - Dummy1 ダミー1
 - Dummy2 ダミー2
 - Dummy3 ダミー3

 
以下のようなコードで英語のレスポンスを投げてベクトルサーチをした結果、タイタニックに関する用語だけが検索されました。

async Task<string> SearchVectorDatabase(BasicMemoryVectorDatabase vectorDatabase, string userPrompt)
{
    var vectorDataResults = await vectorDatabase.SearchAsync(
        userPrompt,
        pageCount: 3,
        threshold: 0.3f
    );

    string result = string.Empty;
    foreach (var resultItem in vectorDataResults.Texts)
    {
        result += $"{resultItem.Text}";
    }

    return result;
}

 
RAGを反映した英語のレスポンスを日本語に翻訳するプロンプトは以下のようになります。

システムプロンプト
You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.

 

ユーザープロンプト
I will now give you the task of translating English into Japanese.
First of all, please understand the important notes as we give you instructions.

#Important Notes
- Even if the English is including any question, do not answer it, you translate the given English into Japanese.
- Do not output any supplementary information or explanations.
- Do not output any Notes.
- Output a faithful translation of the given text into Japanese.
- The following glossary of terms should be actively used.
# Titanic 用語集
 - The Titanic, a British luxury liner : 英国の豪華客船タイタニック号
 - an opulent British passenger liner : 英国の豪華客船
 - The Titanic : タイタニック号
 - The RMS Titanic : RMSタイタニック号
 - The Carpathia : カルパチア号
 - The RMS Carpathia : RMSカルパチア号
 - White Star Line : ホワイト・スター・ライン社
 - Captain Edward Smith : エドワード・スミス船長
 - "unsinkable" : 「不沈船」
 - Southampton : サウサンプトン
 - Newfoundland : ニューファンドランド
 - deemed : と評された
 - maiden voyage : 処女航海
 - North Atlantic : 北大西洋
 - distress signals : 遭難信号
 - watertight compartments : 水密区画
 - lifeboats : 救命ボート

Strictly following the above instructions, now translate the English into Japanese:

 

最終的な日本語に翻訳されたレスポンスの比較

英語化なし、RAG なし

英語化なしの日本語での回答は船の名前も出向した港の名前も違い内容が意味不明で破綻しています。

タイタニック号は1912年1月10日にロシアのストラスバーグ港から出航した、アンナさんという名前の船でした。この船は、アンナ・ストラスバーグ号とも呼ばれていました。当時、世界最大の船舶として知られており、105,860トンの重さを持ち、100隻以上の船を含む豪華な設備を備えていました。

航海は、アンナ・ストラスバーグ号が北極海を航行する旅でした。冬の厚い氷の海を航行するために、船内には多くの導管や氷を切るための機械が設置されていました。この船は、冬の厚い氷の海を航行するために優れた設計と技術で知られていました。

しかし、1月14日、船は巨大な氷山に衝突して沈没しました。この事故は、船内に乗組んだ乗客および船員約1,500人の命を奪いました。この悲劇は、世界史上最大の海上事故として記録されており、その後の航海技術や安全規制に大きな影響を与えました。

タイタニック号の航海は、人類の歴史における重要な航海事件として記録されています。それは、当時の航海技術の限界と、海上事故の深刻な結果を示しています。この事件は、安全規制の強化と航海技術の進歩についての議論を促し、今日まで続いています。

 

英語化あり、RAG なし

英語の時点ではかなり精度が高いですが、固有名詞の翻訳品質が低く最終的な内容は微妙な感じになります。

 タイタニック、英国の乗客船として「沈まない」と認識されたがゆえんの大船は、南英国のサウスアンティオンからニューヨーク市アメリカ合衆国への初航船出発日である4月10日に出発した。この船の指揮を執ったエドワード・スミス長により、乗員および乗客約2,200人を乗せていた。これらには富裕層、移住者、移住者が含まれており、船の旅はその先進的な安全機能、例えば水室と遠隔操作された船舶が含まれていた。

しかし、4月14日の夜、タイタニックは北大西洋海岸にある氷山に衝突した。この衝突により船体の背骨が破損し、前方の水室に水が溢れ始めた。船舶の乗組員が水を排除し、浮力を維持するための努力をしたが、タイタニックはわずか2時間で沈没した。

この災害は約1,500人の命を奪い、歴史上最大の平和時代の海上事故の一つとなった。タイタニックの沈没は、海上安全規則の改善をもたらし、1914年に国際海上安全規則(SOLAS)が設立された。タイタニックの残骸は1985年に12,500フィート以上の深度で発見され、それ以来、海底考古学者が探査している。タイタニックの物語は、人間の創造力の限界と自然の不確実性の暴力の物語として、永続する覚えとなっている。

 

英語化あり、RAG あり

RAG を導入したことで固有名詞のカタカナ表記や滑らかな日本語としての品質が向上しましたが、検証の結果以下の特徴がわかりました。

  • 与える用語が多いと文章のつながりの滑らかさが失われやすい
  • RAG のキーワード数を最低限に絞ることで最大限の品質が出る
 タイタニック号、英国の豪華客船として「不沈船」と評された、サウサンプトンからニューヨーク市へ向かう処女航海を始めた。エドワード・スミス船長の指揮の下、この豪華客船は2,200人の乗員と乗客を乗せ、富裕層、移住者、移住者を含んでいた。タイタニック号の旅は、水密区画や遠隔操作された救命ボートなどの 先進的な安全機能によって特徴付けられていた。

しかし、1912年4月14日の夜、タイタニック号は北大西洋にある氷山に衝突した。この衝突により、船体の前部に水が溢れ、水密区画が破損した。船内の水を排水し、浮力を維持するための努力は行われたが、タイタニック号はわずか2時間で沈没した。

この災害は約1,500人の命を奪い、歴史上最大の平和海上事故の一つとなった。この事故は、船舶安全規則の改善を促し、1914年に国際沿岸安全規則(SOLAS)を設立した。タイタニック号の残骸は1985年に12,500フィート以上の深度で発見され、これまでに海底考古学者によって探査されている。タイタニック号の物語は、人間の創造力の限界と自然の不確実性の象徴として、永続する考えさせられるものである。

 

結論

Phi 自体が英語前提のためでの日本語での問い合わせに対する回答品質はそれほど高くありませんが、

  • Phi 自体を活用してプロンプトを英語に翻訳し問い合わせを行う
  • 英語のレスポンスを RAG で用語補正を行い Phi で日本語に翻訳する

という2つの戦略でローカルの SLM のみを活用しても品質をある程度上げることができました。

ローカル PC のリソースのみで Text Completion を活用するにはまだまだ課題はありますが、今回はそれなりの品質を出せたということで検証を終了としようと思います。
 

コード

最後に完成したソリューションですが、以下のリポジトリにあります。
github.com
 
appsettings.json と Program.cs だけこちらにも掲載しておきます。
 

appsettings.json
{
  "modelPhi35Min128k": "<Your model path>",
  "modelPhi3Med4k": "<Your model path>",
  "modelPhi3Med128k": "<Your model path>",
  "modelPhi3Min4k": "<Your model path>",
  "modelPhi3Min128k": "<Your model path>",
  "modelPhi4": "<Your model path>",
  "modelPhi4Min128k": "<Your model path>",
  "isTranslate": "true",
  "isUsingRag": "true",
  "systemPrompt": "あなたは船舶と航海の歴史に詳しい歴史学者です。与えられた質問にあなたの船舶と航海の歴史の知識を最大限活用して解説してください。",
  "userPrompt": "タイタニック号の航海について最大300字以内で教えてください。",
  "additionalDocumentsPath": "additional-docs"

 

Program.cs
using Microsoft.ML.OnnxRuntimeGenAI;
using System.Diagnostics;
using System.Text;
using Build5Nines.SharpVector;
using Build5Nines.SharpVector.Data;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;

var newLine = Environment.NewLine;

var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
    .Build();

var configuration = builder.Configuration;

var modelPath = new ModelPath(builder);
var prompt = new Prompt(builder);
var option = new Option(builder);

string additionalDocumentsPath = configuration["additionalDocumentsPath"] ?? throw new ArgumentNullException("additionalDocumentsPath is not found");

using OgaHandle ogaHandle = new OgaHandle();

// RAG 用のベクトルデータベースのセットアップ
var additionalDocumentsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, additionalDocumentsPath);
var vectorDatabase = new BasicMemoryVectorDatabase();
LoadAdditionalDocuments(additionalDocumentsDirectory).Wait();
Console.WriteLine();

// モデルのセットアップ
Console.WriteLine($"Loading model:{newLine}{modelPath.Phi35Min128k}");

var sw = Stopwatch.StartNew();
using Model model = new Model(modelPath.Phi35Min128k);
using Tokenizer tokenizer = new Tokenizer(model);
sw.Stop();
 
Console.WriteLine($"{newLine}Model loading time is {sw.Elapsed.Seconds:0.00} sec.\n");

// 翻訳するかどうか
Console.WriteLine($"翻訳する:{newLine}{option.IsTranslate}");
// RAG を使うかどうか
Console.WriteLine($"RAG を使う:{newLine}{option.IsUsingRag}");

// プロンプトのセットアップ
Console.WriteLine($"{newLine}システムプロンプト:{newLine}{prompt.System}");
Console.WriteLine($"{newLine}ユーザープロンプト:{newLine}{prompt.User}{newLine}");

var translatedSystemPrompt = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("Translated System Prompt:");
    await foreach (var translatedPart in Translate(prompt.System, Language.Japanese, Language.English))
    {
        Console.Write(translatedPart);
        translatedSystemPrompt += translatedPart;
    }
    Console.WriteLine($"{newLine}----------------------------------------{newLine}");
}
else
{
    translatedSystemPrompt = prompt.System;
}

var translatedUserPrompt = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("Translated User Prompt:");
    await foreach (var translatedPart in Translate(prompt.User, Language.Japanese, Language.English))
    {
        Console.Write(translatedPart);
        translatedUserPrompt += translatedPart;
    }
    Console.WriteLine($"{newLine}----------------------------------------{newLine}");
}
else
{
    translatedUserPrompt = prompt.User;
}

Console.WriteLine($"{newLine}システムプロンプト:{newLine}{translatedSystemPrompt}");
Console.WriteLine($"{newLine}ユーザープロンプト:{newLine}{translatedUserPrompt}{newLine}");

var sequences = tokenizer.Encode($@"<|system|>{translatedSystemPrompt}<|end|><|user|>{translatedUserPrompt}<|end|><|assistant|>");

// プロンプトを投げて回答を得る
using GeneratorParams generatorParams = new GeneratorParams(model);
generatorParams.SetSearchOption("min_length", 100);
generatorParams.SetSearchOption("max_length", 2000);
//generatorParams.SetSearchOption("temperature", 1.0);
//generatorParams.SetSearchOption("top_k", 0.0);
//generatorParams.SetSearchOption("top_p", 1.0);
generatorParams.SetSearchOption("past_present_share_buffer", false);
generatorParams.TryGraphCaptureWithMaxBatchSize(1);
generatorParams.SetInputSequences(sequences);

using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
StringBuilder stringBuilder = new();

Console.WriteLine("Response:");

var totalTokens = 0;

string part;
sw = Stopwatch.StartNew();
while (!generator.IsDone())
{
    try
    {
        await Task.Delay(50).ConfigureAwait(false);
        generator.ComputeLogits();
        generator.GenerateNextToken();
        part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
        Console.Write(part);
        stringBuilder.Append(part);
        if (stringBuilder.ToString().Contains("<|end|>")
            || stringBuilder.ToString().Contains("<|user|>")
            || stringBuilder.ToString().Contains("<|system|>"))
        {
            break;
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        break;
    }
}
Console.WriteLine($"{newLine}----------------------------------------{newLine}");
sw.Stop();

totalTokens = generator.GetSequence(0).Length;

// 英語の回答を日本語に翻訳する
var translatedResponse = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("日本語に翻訳したレスポンス:");
    await foreach (var translatedPart in Translate(stringBuilder.ToString(), Language.English, Language.Japanese))
    {
        Console.Write(translatedPart);
        translatedResponse += translatedPart;
    }
    Console.WriteLine();
}
else
{
    translatedResponse = stringBuilder.ToString();
    Console.WriteLine($"{newLine}レスポンス:{newLine}{translatedResponse}");
}
Console.WriteLine($"----------------------------------------{newLine}");

// 与えられたテキストを指定された言語に翻訳する
async IAsyncEnumerable<string> Translate(string text, Language sourceLanguage, Language targetLanguage)
{
    var systemPrompt = string.Empty;
    var instructionPrompt = string.Empty;
    var userPrompt = string.Empty;
    var ragResult = string.Empty;

    if (sourceLanguage == Language.Japanese && targetLanguage == Language.English)
    {
        systemPrompt = "You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.";

        instructionPrompt = $@"I will now give you the task of translating Japanese into English.{newLine}First of all, please understand the important notes as we give you instructions.{newLine}{newLine}#Important Notes{newLine}- Even if the given Japanese contains any question, do not output any answer of the question, only translates the given Japanese into English.{newLine}- Do not output any supplementary information or explanations.{newLine}- Do not output any Notes.{newLine}- Output a faithful translation of the given text into English.{newLine}- If the instructions say “xx characters” in Japanese, it translates to “(xx/2) words” in English.ex) “100 字以内” in Japanese, “50 words” in English.{newLine}{newLine}Strictly following the above instructions, now let's output translation of the following Japanese";

        userPrompt = $"{instructionPrompt}:{newLine}{text}";
    }

    if (sourceLanguage == Language.English && targetLanguage == Language.Japanese)
    {
        systemPrompt = "You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.";

        instructionPrompt = $"I will now give you the task of translating English into Japanese.{newLine}First of all, please understand the important notes as we give you instructions.{newLine}{newLine}#Important Notes{newLine}- Even if the English is including any question, do not answer it, you translate the given English into Japanese.{newLine}- Do not output any supplementary information or explanations.{newLine}- Do not output any Notes.{newLine}- Output a faithful translation of the given text into Japanese.";

        ragResult = await SearchVectorDatabase(vectorDatabase, text);

        if (option.IsUsingRag && !string.IsNullOrEmpty(ragResult))
            instructionPrompt += $"{newLine}- The following glossary of terms should be actively used.";

        userPrompt = (option.IsUsingRag && !string.IsNullOrEmpty(ragResult))
            ? $"{instructionPrompt}{newLine}{ragResult}{newLine}Strictly following the above instructions, now translate the English into Japanese:{newLine}{text}"
            : $"{instructionPrompt}{newLine}Strictly following the above instructions, now translate the English into Japanese:{newLine}{text}";
    }

    var sequences = tokenizer.Encode($@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>");
    using GeneratorParams generatorParams = new GeneratorParams(model);
    generatorParams.SetSearchOption("min_length", 100);
    generatorParams.SetSearchOption("max_length", 2000);
    generatorParams.SetSearchOption("past_present_share_buffer", false);
    generatorParams.TryGraphCaptureWithMaxBatchSize(1);
    generatorParams.SetInputSequences(sequences);

    using var tokenizerStream = tokenizer.CreateStream();
    using var generator = new Generator(model, generatorParams);
    StringBuilder stringBuilder = new();
    while (!generator.IsDone())
    {
        string streamingPart = string.Empty;
        try
        {
            await Task.Delay(10).ConfigureAwait(false);
            generator.ComputeLogits();
            generator.GenerateNextToken();
            streamingPart = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
            stringBuilder.Append(streamingPart);
            if (stringBuilder.ToString().Contains("<|end|>")
                || stringBuilder.ToString().Contains("<|user|>")
                || stringBuilder.ToString().Contains("<|system|>"))
            {
                break;
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
            break;
        }
        yield return streamingPart;
    }
}

async Task LoadAdditionalDocuments(string directoryPath)
{
    Console.WriteLine($"Loading Additional Documents:");
    var files = Directory.GetFiles(directoryPath, "*.*", SearchOption.AllDirectories)
                             .Where(f => f.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) ||
                                         f.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ||
                                         f.EndsWith(".mdx", StringComparison.OrdinalIgnoreCase)).ToArray();

    var vectorDataLoader = new TextDataLoader<int, string>(vectorDatabase);
    var tasks = files.Select(async file =>
    {
        Console.WriteLine($"{file}");
        if (System.IO.File.Exists(file))
        {
            var fileContents = await System.IO.File.ReadAllTextAsync(file);
            await vectorDataLoader.AddDocumentAsync(fileContents, new TextChunkingOptions<string>
            {
                Method = TextChunkingMethod.Paragraph,
                RetrieveMetadata = (chunk) => file
            });
        }
    });
    await Task.WhenAll(tasks);
}

async Task<string> SearchVectorDatabase(BasicMemoryVectorDatabase vectorDatabase, string userPrompt)
{
    var vectorDataResults = await vectorDatabase.SearchAsync(
        userPrompt,
        pageCount: 3,
        threshold: 0.3f
    );

    string result = string.Empty;
    foreach (var resultItem in vectorDataResults.Texts)
    {
        result += $"{resultItem.Text}{newLine}";
    }

    return result;
}

public sealed class ModelPath
{
    private readonly string modelPhi35Min128k;
    private readonly string modelPhi3Med4k;
    private readonly string modelPhi3Med128k;
    private readonly string modelPhi3Min4k;
    private readonly string modelPhi3Min128k;
    private readonly string modelPhi4;
    private readonly string modelPhi4Min128k;

    public ModelPath(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;

        modelPhi35Min128k = configuration["modelPhi35Min128k"] ?? throw new ArgumentNullException("modelPhi35Min128k is not found.");
        modelPhi3Med4k = configuration["modelPhi3Med4k"] ?? throw new ArgumentNullException("modelPhi3Med4k is not found.");
        modelPhi3Med128k = configuration["modelPhi3Med128k"] ?? throw new ArgumentNullException("modelPhi3Med128k is not found.");
        modelPhi3Min4k = configuration["modelPhi3Min4k"] ?? throw new ArgumentNullException("modelPhi3Min4k is not found.");
        modelPhi3Min128k = configuration["modelPhi3Min128k"] ?? throw new ArgumentNullException("modelPhi3Min128k is not found.");
        modelPhi4 = configuration["modelPhi4"] ?? throw new ArgumentNullException("modelPhi4 is not found.");
        modelPhi4Min128k = configuration["modelPhi4Min128k"] ?? throw new ArgumentNullException("modelPhi4Min128k is not found.");
    }

    public string Phi35Min128k { get => modelPhi35Min128k; }
    public string Phi3Med4k { get => modelPhi3Med4k; }
    public string Phi3Med128k { get => modelPhi3Med128k; }
    public string Phi3Min4k { get => modelPhi3Min4k; }
    public string Phi3Min128k { get => modelPhi3Min128k; }
    public string Phi4 { get => modelPhi4; }
    public string Phi4Min128k { get => modelPhi4Min128k; }
}

public sealed class Prompt
{
    private readonly string systemPrompt;
    private readonly string userPrompt;

    public Prompt(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;

        systemPrompt = configuration["systemPrompt"] ?? throw new ArgumentNullException("systemPrompt is not found.");
        userPrompt = configuration["userPrompt"] ?? throw new ArgumentNullException("userPrompt is not found.");
    }

    public string System { get => systemPrompt; }
    public string User { get => userPrompt; }
}

public sealed class Option
{
    private readonly bool isTranslate;
    private readonly bool isUsingRag;

    public Option(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;
        isTranslate = bool.TryParse(configuration["isTranslate"] ?? throw new ArgumentNullException("isTranslate is not found."), out var resultIsTranslate) && resultIsTranslate;
        isUsingRag = bool.TryParse(configuration["isUsingRag"] ?? throw new ArgumentNullException("isUsingRag is not found."), out var resultIsUsingRag) && resultIsUsingRag;
    }

    public bool IsTranslate { get => isTranslate; }
    public bool IsUsingRag { get => isUsingRag; }
}

public enum Language
{
    Japanese,
    English
}

 

C# を使って CPU で Edge AI を試してみよう(3)日本語で精度を高めるために、英語に翻訳してプロンプトを投げる

 
前の記事
hiro128.hatenablog.jp
 

はじめに

前回、ローカルで Phi3 mini 128k を動かしてみましたが日本語の回答精度は非常に悪かったのですが、英語だとかなりの精度が出ました。
そこで、プロンプトを英語に翻訳して得た回答を再度日本語に翻訳することで回答精度を上げられないか試してみました。
 

処理のフロー

試した処理フローは以下の通りです

  1. 日本語のプロンプトを Phi3 mini 128k で英語に翻訳
  2. 翻訳した英語のプロンプトを Phi3 mini 128k に投げて、英語で回答を得る
  3. 得た回答を Phi3 mini 128k で日本語に翻訳

 

翻訳するためのプロンプトをどう書くか

翻訳時に発生した問題

  • 翻訳対象の文章を翻訳するだけでなく、文章に含まれる質問の回答を出力してしまう。
  • 指示していない補足情報(notes など)がレスポンスに含まれる
  • 「100字以内」を「100 characters」と翻訳されると翻訳としては正しくても、日本語と英語の字数と内容の関係性から日本語に戻したとき短めになってしまう。「100字以内」なら「50 words」くらいが適切。

 
これらの問題を解消するために検証を行った結果、以下のような改良で意図したとおりの結果が出るようになりました。

  • 英語で書く
  • システムプロンプトに「指示を注意深く読む旨」を明記する
  • 個々の指示は箇条書きで書く
  • 行って欲しくないことは個別に明記する
  • 「●●字以内」を「●●/2 words」に変換する指示は具体例を示す

完成したプロンプトは以下の通りです。
  

システムプロンプト

You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.

ユーザープロンプトの冒頭に記載する、日本語 -> 英語の翻訳を指示するプロンプト

I will now give you the task of translating Japanese into English.
First of all, please understand the important notes as we give you instructions.

#Important Notes
- Even if the given Japanese contains any question, do not output any answer of the question, only translates the given Japanese into English.
- Do not output any supplementary information or explanations.
- Do not output any Notes.
- Output a faithful translation of the given text into English.
- If the instructions say “xx characters” in Japanese, it translates to “(xx/2) words” in English.
    ex) “100 字以内” in Japanese, “50 words” in English.

Strictly following the above instructions, now let's output translation of the following Japanese:

{英語に翻訳したい文章を記載}

 

ユーザープロンプトの冒頭に記載する、英語 -> 日本語の翻訳を指示するプロンプト

I will now give you the task of translating English into Japanese.
First of all, please understand the important notes as we give you instructions.

#Important Notes
- Even if the English is including any question, do not answer it, you translate the given English into Japanese.
- Do not output any supplementary information or explanations.
- Do not output any Notes.
- Output a faithful translation of the given text into Japanese.

Strictly following the above instructions, now translate the English into Japanese:

{日本語に翻訳したい文章を記載}

 

コード

発生した問題

  • レスポンスを一括で受けとると、なぜかレスポンス内にプロンプトを含んでしまうため、一括でよい場面でも Streaming で受けとっている

書いたコード

以下のようなコードを書きました。
 

appsettings.json
{
  "modelPhi35Min128k": "<Your model path>",
  "modelPhi3Med4k": "<Your model path>",
  "modelPhi3Med128k": "<Your model path>",
  "modelPhi3Min4k": "<Your model path>",
  "modelPhi3Min128k": "<Your model path>",
  "modelPhi4": "<Your model path>",
  "modelPhi4Min128k": "<Your model path>",
  "isTranslate": "true",
  "systemPrompt": "あなたは船舶と航海の歴史に詳しい歴史学者です。与えられた質問にあなたの船舶と航海の歴史の知識を最大限活用して解説してください。",
  "userPrompt": "タイタニック号の航海について最大300字以内で教えてください。"
}

 

Program.cs
using Microsoft.ML.OnnxRuntimeGenAI;
using System.Diagnostics;
using System.Text;
using Build5Nines.SharpVector;
using Build5Nines.SharpVector.Data;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;

var newLine = Environment.NewLine;

var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
    .Build();

var configuration = builder.Configuration;

var modelPath = new ModelPath(builder);
var prompt = new Prompt(builder);
var option = new Option(builder);

string additionalDocumentsPath = configuration["additionalDocumentsPath"] ?? throw new ArgumentNullException("additionalDocumentsPath is not found");

using OgaHandle ogaHandle = new OgaHandle();

// RAG 用のベクトルデータベースのセットアップ
var additionalDocumentsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, additionalDocumentsPath);
var vectorDatabase = new BasicMemoryVectorDatabase();
LoadAdditionalDocuments(additionalDocumentsDirectory).Wait();
Console.WriteLine();

// モデルのセットアップ
Console.WriteLine($"Loading model:{newLine}{modelPath.Phi3Min128k}");

var sw = Stopwatch.StartNew();
using Model model = new Model(modelPath.Phi3Min128k);
using Tokenizer tokenizer = new Tokenizer(model);
sw.Stop();
 
Console.WriteLine($"{newLine}Model loading time is {sw.Elapsed.Seconds:0.00} sec.\n");

// 翻訳するかどうか
Console.WriteLine($"翻訳する:{newLine}{option.IsTranslate}");

// プロンプトのセットアップ
Console.WriteLine($"{newLine}システムプロンプト:{newLine}{prompt.System}");
Console.WriteLine($"{newLine}ユーザープロンプト:{newLine}{prompt.User}{newLine}");

var translatedSystemPrompt = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("Translated System Prompt:");
    await foreach (var translatedPart in Translate(prompt.System, Language.Japanese, Language.English))
    {
        Console.Write(translatedPart);
        translatedSystemPrompt += translatedPart;
    }
    Console.WriteLine($"{newLine}----------------------------------------{newLine}");
}
else
{
    translatedSystemPrompt = prompt.System;
}

var translatedUserPrompt = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("Translated User Prompt:");
    await foreach (var translatedPart in Translate(prompt.User, Language.Japanese, Language.English))
    {
        Console.Write(translatedPart);
        translatedUserPrompt += translatedPart;
    }
    Console.WriteLine($"{newLine}----------------------------------------{newLine}");
}
else
{
    translatedUserPrompt = prompt.User;
}

Console.WriteLine($"{newLine}システムプロンプト:{newLine}{translatedSystemPrompt}");
Console.WriteLine($"{newLine}ユーザープロンプト:{newLine}{translatedUserPrompt}{newLine}");

var sequences = tokenizer.Encode($@"<|system|>{translatedSystemPrompt}<|end|><|user|>{translatedUserPrompt}<|end|><|assistant|>");

// プロンプトを投げて回答を得る
using GeneratorParams generatorParams = new GeneratorParams(model);
generatorParams.SetSearchOption("min_length", 100);
generatorParams.SetSearchOption("max_length", 2000);
generatorParams.SetSearchOption("past_present_share_buffer", false);
generatorParams.TryGraphCaptureWithMaxBatchSize(1);
generatorParams.SetInputSequences(sequences);

using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
StringBuilder stringBuilder = new();

Console.WriteLine("Response:");

var totalTokens = 0;

string part;
sw = Stopwatch.StartNew();
while (!generator.IsDone())
{
    try
    {
        await Task.Delay(50).ConfigureAwait(false);
        generator.ComputeLogits();
        generator.GenerateNextToken();
        part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
        Console.Write(part);
        stringBuilder.Append(part);
        if (stringBuilder.ToString().Contains("<|end|>")
            || stringBuilder.ToString().Contains("<|user|>")
            || stringBuilder.ToString().Contains("<|system|>"))
        {
            break;
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        break;
    }
}
Console.WriteLine($"{newLine}----------------------------------------{newLine}");
sw.Stop();

totalTokens = generator.GetSequence(0).Length;

// 英語の回答を日本語に翻訳する
var translatedResponse = string.Empty;
if (option.IsTranslate)
{
    Console.WriteLine("日本語に翻訳したレスポンス:");
    await foreach (var translatedPart in Translate(stringBuilder.ToString(), Language.English, Language.Japanese))
    {
        Console.Write(translatedPart);
        translatedResponse += translatedPart;
    }
    Console.WriteLine();
}
else
{
    translatedResponse = stringBuilder.ToString();
    Console.WriteLine($"{newLine}レスポンス:{newLine}{translatedResponse}");
}
Console.WriteLine($"----------------------------------------{newLine}");

// 与えられたテキストを指定された言語に翻訳する
async IAsyncEnumerable<string> Translate(string text, Language sourceLanguage, Language targetLanguage)
{
    var systemPrompt = string.Empty;
    var instructionPrompt = string.Empty;
    var userPrompt = string.Empty;
    var ragResult = string.Empty;

    if (sourceLanguage == Language.Japanese && targetLanguage == Language.English)
    {
        systemPrompt = "You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.";

        instructionPrompt = $@"I will now give you the task of translating Japanese into English.{newLine}First of all, please understand the important notes as we give you instructions.{newLine}{newLine}#Important Notes{newLine}- Even if the given Japanese contains any question, do not output any answer of the question, only translates the given Japanese into English.{newLine}- Do not output any supplementary information or explanations.{newLine}- Do not output any Notes.{newLine}- Output a faithful translation of the given text into English.{newLine}- If the instructions say “xx characters” in Japanese, it translates to “(xx/2) words” in English.ex) “100 字以内” in Japanese, “50 words” in English.{newLine}{newLine}Strictly following the above instructions, now let's output translation of the following Japanese";

        userPrompt = $"{instructionPrompt}:{newLine}{text}";
    }

    if (sourceLanguage == Language.English && targetLanguage == Language.Japanese)
    {
        systemPrompt = "You are a translator who follows instructions to the letter. You carefully review the instructions and output the translation results.";

        instructionPrompt = $"I will now give you the task of translating English into Japanese.{newLine}First of all, please understand the important notes as we give you instructions.{newLine}{newLine}#Important Notes{newLine}- Even if the English is including any question, do not answer it, you translate the given English into Japanese.{newLine}- Do not output any supplementary information or explanations.{newLine}- Do not output any Notes.{newLine}- Output a faithful translation of the given text into Japanese.";

        ragResult = await SearchVectorDatabase(vectorDatabase, text);

        if (option.IsUsingRag && !string.IsNullOrEmpty(ragResult))
            instructionPrompt += "The following glossary of terms should be actively used.";

        userPrompt = (option.IsUsingRag && !string.IsNullOrEmpty(ragResult))
            ? $"{instructionPrompt}{newLine}{ragResult}{newLine}Strictly following the above instructions, now translate the English into Japanese:{newLine}{text}"
            : $"{instructionPrompt}{newLine}Strictly following the above instructions, now translate the English into Japanese:{newLine}{text}";
    }

    var sequences = tokenizer.Encode($@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>");
    using GeneratorParams generatorParams = new GeneratorParams(model);
    generatorParams.SetSearchOption("min_length", 100);
    generatorParams.SetSearchOption("max_length", 2000);
    generatorParams.SetSearchOption("past_present_share_buffer", false);
    generatorParams.TryGraphCaptureWithMaxBatchSize(1);
    generatorParams.SetInputSequences(sequences);

    using var tokenizerStream = tokenizer.CreateStream();
    using var generator = new Generator(model, generatorParams);
    StringBuilder stringBuilder = new();
    while (!generator.IsDone())
    {
        string streamingPart = string.Empty;
        try
        {
            await Task.Delay(10).ConfigureAwait(false);
            generator.ComputeLogits();
            generator.GenerateNextToken();
            streamingPart = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
            stringBuilder.Append(streamingPart);
            if (stringBuilder.ToString().Contains("<|end|>")
                || stringBuilder.ToString().Contains("<|user|>")
                || stringBuilder.ToString().Contains("<|system|>"))
            {
                break;
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
            break;
        }
        yield return streamingPart;
    }
}

async Task LoadAdditionalDocuments(string directoryPath)
{
    Console.WriteLine($"Loading Additional Documents:");
    var files = Directory.GetFiles(directoryPath, "*.*", SearchOption.AllDirectories)
                             .Where(f => f.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) ||
                                         f.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ||
                                         f.EndsWith(".mdx", StringComparison.OrdinalIgnoreCase)).ToArray();

    var vectorDataLoader = new TextDataLoader<int, string>(vectorDatabase);
    var tasks = files.Select(async file =>
    {
        Console.WriteLine($"{file}");
        if (System.IO.File.Exists(file))
        {
            var fileContents = await System.IO.File.ReadAllTextAsync(file);
            await vectorDataLoader.AddDocumentAsync(fileContents, new TextChunkingOptions<string>
            {
                Method = TextChunkingMethod.Paragraph,
                RetrieveMetadata = (chunk) => file
            });
        }
    });
    await Task.WhenAll(tasks);
}

async Task<string> SearchVectorDatabase(BasicMemoryVectorDatabase vectorDatabase, string userPrompt)
{
    var vectorDataResults = await vectorDatabase.SearchAsync(
        userPrompt,
        pageCount: 3,
        threshold: 0.3f
    );

    string result = string.Empty;
    foreach (var resultItem in vectorDataResults.Texts)
    {
        result += $"{resultItem.Text}{newLine}";
    }

    return result;
}

public sealed class ModelPath
{
    private readonly string modelPhi35Min128k;
    private readonly string modelPhi3Med4k;
    private readonly string modelPhi3Med128k;
    private readonly string modelPhi3Min4k;
    private readonly string modelPhi3Min128k;
    private readonly string modelPhi4;
    private readonly string modelPhi4Min128k;

    public ModelPath(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;

        modelPhi35Min128k = configuration["modelPhi35Min128k"] ?? throw new ArgumentNullException("modelPhi35Min128k is not found.");
        modelPhi3Med4k = configuration["modelPhi3Med4k"] ?? throw new ArgumentNullException("modelPhi3Med4k is not found.");
        modelPhi3Med128k = configuration["modelPhi3Med128k"] ?? throw new ArgumentNullException("modelPhi3Med128k is not found.");
        modelPhi3Min4k = configuration["modelPhi3Min4k"] ?? throw new ArgumentNullException("modelPhi3Min4k is not found.");
        modelPhi3Min128k = configuration["modelPhi3Min128k"] ?? throw new ArgumentNullException("modelPhi3Min128k is not found.");
        modelPhi4 = configuration["modelPhi4"] ?? throw new ArgumentNullException("modelPhi4 is not found.");
        modelPhi4Min128k = configuration["modelPhi4Min128k"] ?? throw new ArgumentNullException("modelPhi4Min128k is not found.");
    }

    public string Phi35Min128k { get => modelPhi35Min128k; }
    public string Phi3Med4k { get => modelPhi3Med4k; }
    public string Phi3Med128k { get => modelPhi3Med128k; }
    public string Phi3Min4k { get => modelPhi3Min4k; }
    public string Phi3Min128k { get => modelPhi3Min128k; }
    public string Phi4 { get => modelPhi4; }
    public string Phi4Min128k { get => modelPhi4Min128k; }
}

public sealed class Prompt
{
    private readonly string systemPrompt;
    private readonly string userPrompt;

    public Prompt(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;

        systemPrompt = configuration["systemPrompt"] ?? throw new ArgumentNullException("systemPrompt is not found.");
        userPrompt = configuration["userPrompt"] ?? throw new ArgumentNullException("userPrompt is not found.");
    }

    public string System { get => systemPrompt; }
    public string User { get => userPrompt; }
}

public sealed class Option
{
    private readonly bool isTranslate;
    private readonly bool isUsingRag;

    public Option(HostApplicationBuilder builder)
    {
        var configuration = builder.Configuration;
        isTranslate = bool.TryParse(configuration["isTranslate"] ?? throw new ArgumentNullException("isTranslate is not found."), out var resultIsTranslate) && resultIsTranslate;
        isUsingRag = bool.TryParse(configuration["isUsingRag"] ?? throw new ArgumentNullException("isUsingRag is not found."), out var resultIsUsingRag) && resultIsUsingRag;
    }

    public bool IsTranslate { get => isTranslate; }
    public bool IsUsingRag { get => isUsingRag; }
}

public enum Language
{
    Japanese,
    English
}

 

結果

  • 英語でプロンプトを投げるため日本語で問い合わせるより英語の時点での回答精度はかなり高い
  • しかし、英語で受けとった回答に翻訳を通すためどうしても固有名詞や言い回しがうまく日本語に翻訳できず最終的な結果は微妙になってしまう
システムプロンプト
あなたは船舶と航海の歴史に詳しい歴史学者です。与えられた質問にあなたの船舶と航海の歴史の知識を最大限活用して解説してください。

 

ユーザープロンプト
タイタニック号の航海について最大300字以内で教えてください。

 

レスポンス(英語)
 The Titanic, a British luxury liner, embarked on its maiden voyage in 1912, aiming to connect America and Europe. Built by the White Star Line, it was deemed "unsinkable" due to its advanced safety features. However, on April 14th, it struck an iceberg in the North Atlantic. Despite distress signals, the Titanic sank in just over two hours, leading to the deaths of more than 1,500 passengers and crew. Survivors were rescued by the nearby RMS Carpathia, sparking global mourning and leading to significant changes in maritime safety regulations, including the establishment of the International Convention for the Safety of Life at Sea (SOLAS) in 1914. The Titanic's tragic voyage remains a poignant symbol of human vulnerability and the importance of safety in seafaring.

 

レスポンス(日本語)
 ティタニック、英国の優雅な船であり、1912年に初航行を計画していました。ホワイトオーストライト・ラインによって作られ、"unsinkable"と評され、請求されていました。しかし、1912年4月14 日、北アトランティスのアイスヌートに倒しました。警報が出されたものの、2時間以上、妬りました。その後、ホワイトオーストライト・ラインのRMSカルパディアによって致命的な救助が行われ、 世界的な哀悼が始まり、航海の安全規約に関する規定の変化が起こり、1914年に設立された国際生命を守る船の安全規約(SOLAS)の設立につながりました。ティタニックの悲劇的な航路は、人間の脆弱性と航海における安全の重要性を象徴するものです。

翻訳が微妙な箇所

  • ティタニック -> タイタニック
  • "unsinkable" -> 「不沈船」
  • ホワイトオーストライト・ライン -> ホワイト・スター・ライン
  • RMSカルパディアによって致命的な救助が行われ -> 生存者は近くにいたRMSカルパチアによって救助された

 

結論

英語の回答は正確であり、日本語の翻訳精度に関しては主に固有名詞や微妙な言い回しの対応がうまくいっていない状況。さらに RAG を導入し固有名詞や微妙な言い回しを正しく翻訳できるようにして、精度を高められるかどうか検証を続けてみます。
 
次の記事
hiro128.hatenablog.jp

C# を使って CPU で Edge AI を試してみよう(2)簡単なアプリを作成してみる

 
前の記事
hiro128.hatenablog.jp
 

はじめに

いまさらながらですが、Phi 3 を試してみるために、ローカルで実行してみるコンソールアプリを作ってみました。
 
リポジトリはこちらです。
github.com
 
モデルは Hugging Face から DL してください。
DL する cli のコマンドのサンプルは前回の記事を参照ください。
hiro128.hatenablog.jp
 

コード

以下のページを参考にコードを作成しました。
learn.microsoft.com
モデルをメモリに展開して、システムプロンプトとユーザープロンプトを投げて回答ストリーミングで表示するだけのシンプルなコードです

using Microsoft.Extensions.Configuration;
using Microsoft.ML.OnnxRuntimeGenAI;
using System.Diagnostics;
using System.Text;


var env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty;
var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .AddJsonFile($"appsettings.{env}.json", true)
    .Build();

string modelPhi35Min128k = configuration["modelPhi35Min128k"] ?? throw new ArgumentNullException("modelPhi35Min128k is not found.");
string modelPhi3Med4k = configuration["modelPhi3Med4k"] ?? throw new ArgumentNullException("modelPhi3Med4k is not found.");
string modelPhi3Med128k = configuration["modelPhi3Med128k"] ?? throw new ArgumentNullException("modelPhi3Med128k is not found.");
string modelPhi3Min4k = configuration["modelPhi3Min4k"] ?? throw new ArgumentNullException("modelPhi3Min4k is not found.");
string modelPhi3Min128k = configuration["modelPhi3Min128k"] ?? throw new ArgumentNullException("modelPhi3Min128k is not found.");

var systemPrompt = configuration["systemPrompt"] ?? throw new ArgumentNullException("systemPrompt is not found.");
var userPrompt = configuration["userPrompt"] ?? throw new ArgumentNullException("userPrompt is not found.");

using OgaHandle ogaHandle = new OgaHandle();

// モデルのセットアップ
var modelPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, modelPhi3Med128k);

var sw = Stopwatch.StartNew();
using Model model = new Model(modelPath);
using Tokenizer tokenizer = new Tokenizer(model);
sw.Stop();

Console.WriteLine($"\r\nModel loading time is {sw.Elapsed.Seconds:0.00} sec.\r\n");

// プロンプトのセットアップ
Console.WriteLine($"\r\nシステムプロンプト:\r\n{systemPrompt}");
Console.WriteLine($"\r\nユーザープロンプト:\r\n{userPrompt}\r\n");

var sequences = tokenizer.Encode($@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>");

// プロンプトを投げて回答を得る
using GeneratorParams generatorParams = new GeneratorParams(model);
generatorParams.SetSearchOption("min_length", 100);
generatorParams.SetSearchOption("max_length", 2000);
generatorParams.TryGraphCaptureWithMaxBatchSize(1);
generatorParams.SetInputSequences(sequences);

using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
StringBuilder stringBuilder = new();

Console.WriteLine("レスポンス:");

var totalTokens = 0;

string part;
sw = Stopwatch.StartNew();
while (!generator.IsDone())
{
    try
    {
        await Task.Delay(10).ConfigureAwait(false);
        generator.ComputeLogits();
        generator.GenerateNextToken();
        part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
        Console.Write(part);
        stringBuilder.Append(part);
        if (stringBuilder.ToString().Contains("<|end|>")
            || stringBuilder.ToString().Contains("<|user|>")
            || stringBuilder.ToString().Contains("<|system|>"))
        {
            break;
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        break;
    }
}
Console.WriteLine("\r\n");
sw.Stop();

totalTokens = generator.GetSequence(0).Length;

Console.WriteLine($"Streaming Tokens: {totalTokens} - Time: {sw.Elapsed.Seconds:0.00} sec");
Console.WriteLine($"Tokens per second: {((double)totalTokens / sw.Elapsed.TotalSeconds):0.00} tokens");

 

実行環境

  • CPU : Snapdragon X Elite
  • RAM : 16GB
  • CPU : Core Ultra7 155H
  • RAM : 64GB

速度・精度

速度は3回試行し、真ん中の値ですのであくまでも参考程度とお考え下さい。

Phi 3
速度 日本語精度 英語精度
Mini 4k 23.19 tokens/sec. X
Mini 128k 25.49 tokens/sec. X
Medium 4k 5.95 tokens/sec.
Medium 128k 6.24 tokens/sec.

 

プロンプトとレスポンスの一例

Phi 3 Mini 128k(日本語)

回答の内容は不正確で破綻しています。

システムプロンプト:
あなたは船舶と航海の歴史に詳しい歴史学者です。与えられた質問にあなたの船舶と航海の歴史の知識を最大限活用して解説してください。

ユーザープロンプト:
タイタニック号の航海について最大300字以内で教えてください。

レスポンス:
タイタニック号は、1929年に日本の船舶大手東洋販売社によって設計された、世界初の大型軽艘船です。彼らは、日本海外の貿易と 旅行に革新をもたらしました。タイタニック号は、特に東京からパリまでの航路で知られ、その長い航路は日本航天の航海史において重要な役割を果たしました。しかし、1954年の航海中に海底沖縄に墜落し、その後、多くの探検により、救助作戦として知られるようになりました。この船は、日本航天の歴史における重要な転機を象徴しており、その存在は日本航天の発展において不可欠な存在です。

 

Phi 3 Mini 128k(英語)

回答の内容は正確です。(Phi 3 は英語で精度が出るように調整されているので順当な結果ではありますが)

システムプロンプト:
You are a historian with an intimate knowledge of the history of ships and navigation. Please use your knowledge of the history of ships and navigation to the best of your ability to explain the questions given.

ユーザープロンプト:
Please tell us about the voyage of the RMS Titanic in a maximum of 150 words.

レスポンス:
 The RMS Titanic, a British luxury liner, embarked on its maiden voyage from Southampton, England, on April 10, 1912. Designed to be the pinnacle of maritime engineering, it was deemed "unsinkable." However, on its journey to New York City, it tragically struck an iceberg in the North Atlantic Ocean. Despite the distress signals, the Titanic sank on April 15, claiming over 1,500 lives. The disaster highlighted the ship's insufficient lifeboats and the crew's inadequate emergency response. The Titanic's sinking led to significant changes in maritime safety regulations, including the establishment of the International Convention for the Safety of Life at Sea (SOLAS) in 1914. The Titanic's story remains a poignant reminder of human vulnerability and the importance of safety in the face of nature's unpredictability.

 

Phi 3 Medium 128k(日本語)

回答の内容は一部が不正確ですが、おおむね合っています。

システムプロンプト:
あなたは船舶と航海の歴史に詳しい歴史学者です。与えられた質問にあなたの船舶と航海の歴史の知識を最大限活用して解説してください。

ユーザープロンプト:
タイタニック号の航海について最大300字以内で教えてください。

レスポンス:
タイタニック号は、1912年4月10日にイギリスの南ウェールズのバーケンヘッドから出航し、ニューヨーク市へ向かった。この豪華客船は、白い巨人として知られ、当時世界最大の客船であった。しかし、4月14日の夜、北大西洋の氷山に衝突し、氷山の氷によって押し潰された。船はわずか2時間40分で沈没し、乗客乗員1,517人が犠牲になった。この悲劇は、船の設計上の欠陥、船長の誤った判断、氷山の警告を無視したことなど、複数の要因が組み合わさった結果であった。タイタニック号の沈没は、海事史上最も悲劇的な出来事の一つとして記憶されている。

 

Phi 3 Medium 128k(英語)

回答の内容は正確です。(Phi 3 は英語で精度が出るように調整されているので順当な結果ではありますが)

システムプロンプト:
You are a historian with an intimate knowledge of the history of ships and navigation. Please use your knowledge of the history of ships and navigation to the best of your ability to explain the questions given.

ユーザープロンプト:
Please tell us about the voyage of the RMS Titanic in a maximum of 150 words.

レスポンス:
 The RMS Titanic, a British luxury passenger liner, embarked on its maiden voyage from Southampton, England, to New York City, USA, on April 10, 1 vice-admiral of the Royal Navy, Edward Smith, as captain. The ship, deemed "unsinkable," carried 2,224 passengers and crew. On April 14, 1912, Titanic struck an iceberg in the North Atlantic Ocean, causing the ship to sink in less than three hours. Of the passengers and crew, only 710 survived, thanks to lifeboats and the Carpathia, a nearby ship that rescued survivors. The tragedy led to significant improvements in maritime safety, including the International Convention for the Safety of Life at Sea (SOLAS) and the establishment of the International Ice Patrol. The wreck of the Titanic was discovered in 1985, lying at a depth of about 12,500 feet.

 

速度と精度の両立を狙うための改善を考えてみる

  • Phi 3 mini
    • メモリ 16 GB でも実用的な速度は出る
    • 日本語での回答精度は話にならないほど低い
    • 英語での回答精度はかなり高い
  • Phi 3 medium
    • メモリ 16GB では空きメモリがほとんどゼロになるが、かろうじて待てるくらいの速度は出るが常用はかなりストレス
    • メモリ 32GB であればメモリは足りるが、速度はあまり改善しない。
    • 日本語での回答精度もかなり高い。
    • 英語での回答精度はかなり高い
  • 英語なら Phi 3 mini でもかなり精度が高いので、【日本語のプロンプト -> 英語に翻訳して投げる -> 英語のレスポンスを再度日本語に翻訳】という方法で速度と精度の両立を狙えないか検証してみたい(次の記事で検証)

 
次の記事
hiro128.hatenablog.jp