👻

ローカルLLMを翻訳apiを通して日本語化

2024/02/14に公開

前回ローカルLLM APIサーバーを建てました
https://zenn.dev/emoclew/articles/9dfa7fdん948a18b

性能的にはphi-2が良さそうなのですが、日本語の入出力に対応していませんでした
そこで、前に立てた翻訳APIサーバーを間に噛ませることで、日本語対応を目指します

(そもそも、翻訳はかなり難しいタスクらしく、LLMのみの翻訳でGoogle翻訳やDeepLを超えることは、現状では無理みたいです。
ですから、日本語対応しているLLMであっても、英語で入出力をして、間に翻訳機を噛ませたほうが、性能は上がると思います)

プログラム

前回建てたLLMサーバーとほとんど同じですので、サーバー構築は前回の記事を参考にしてください。また、内部で自作の翻訳APIにリクエストを飛ばしますが、これも自分で好きなAPIを利用して構いません。
LLMサーバーを立てる話
https://zenn.dev/emoclew/articles/9dfa7fdん948a18b
翻訳APIサーバーを立てる話
https://zenn.dev/emoclew/articles/bdc0cb017a0ccf

前回からの変更点は2つのファイルのみです

app/llama_cpp_switch.py

前回、実装の章にて、グローバル変数でLLMインスタンスを読み込んだほうがよいという結論をだしました。それに伴い、call_llm関数たちも、llm_typeの引数を必要としなくなったため、削除しました。

llama_cpp_switch.py
from llama_cpp import Llama

llm = Llama(f'models/phi-2.Q4_0.gguf')
def call_llm(text:str, max_tokens: int, end_texts: str):
    output = llm(text, max_tokens=max_tokens, stop=end_texts)
    out_text = output['choices'][0]['text']
    return out_text

def call_llm_system_user_assistant(text:str, max_tokens: int, end_texts: str):
    prompt = (
        '<|system|>\n'
        'You are an excellent AI assistant.\n'
        '<|user|>\n'
        f'{text}\n'
        '<|assistant|>\n'
    )
    end_texts += ['<|system|>', '<|user|>', '<|assistant|>']
    output = llm(prompt, stop=end_texts, max_tokens=max_tokens)
    out_text = output['choices'][0]['text']
    return out_text

app/main.py

前回のapiに継ぎ足して、llm_v2というURLでリクエストを受け付けられるようにします

やっていることはとても単純で、

  1. 日本語の指示文が来る
  2. 翻訳機で英語に翻訳する
  3. 英語の指示文をLLMに入れる
  4. LLMから英語の出力文が出る
  5. 翻訳機で日本語に翻訳する
  6. 日本語の出力文を返す

と、間に翻訳機(api)を入れただけの構成としました。

translator_api_server_ipは建てた翻訳apiサーバーのipを設定してください。

main.py
from fastapi import FastAPI
from pydantic import BaseModel

import app.llama_cpp_switch as llama_cpp_switch
import requests
import json
translator_api_server_ip = '192.168.x.xxx'

class Info(BaseModel):
    llm_input_type : str
    text: str
    max_tokens: int
    end_texts : list

app = FastAPI()

@app.post('/api/llm')
async def llm(info: Info):
    ret_text = ''
    if info.llm_input_type == 'ai_assistant':
        ret_text = llama_cpp_switch.call_llm_system_user_assistant(info.text, info.max_tokens, info.end_texts)
    if info.llm_input_type == 'raw':
        ret_text = llama_cpp_switch.call_llm(info.text, info.max_tokens, info.end_texts)
    ret = {'text':ret_text}
    return ret

class TranslateInfo(BaseModel):
    source_lang: str
    llm_lang: str
    tranalator_type: str
    llm_input_type : str
    text: str
    max_tokens: int
    end_texts : list

@app.post('/api/llm_v2')
async def llm(info: TranslateInfo):
    input_text = info.text
    if info.source_lang != info.llm_lang:
        input_text = translate_text(info.source_lang, info.llm_lang, info.text, info.tranalator_type)
    ret_text = ''
    if info.llm_input_type == 'ai_assistant':
        ret_text = llama_cpp_switch.call_llm_system_user_assistant(input_text, info.max_tokens, info.end_texts)
    if info.llm_input_type == 'raw':
        ret_text = llama_cpp_switch.call_llm(input_text, info.max_tokens, info.end_texts)

    if info.source_lang != info.llm_lang:
        ret_text = translate_text(info.llm_lang, info.source_lang, ret_text, info.tranalator_type)
    ret = { 'text':ret_text}
    return ret

def translate_text(source_lang: str, target_lang: str, text: str, translator_type: str):
    url = f"http://{translator_api_server_ip}:8282/api/translate"
    info = {
        "translator_type":translator_type,
        "source_lang":source_lang,
        "target_lang":target_lang,
        "text":text
    }
    ret = requests.post(url, json.dumps(info))
    ret_info = json.loads(ret.text)
    return ret_info['text']

検証

それでは、その性能と実行速度を見てみましょう

テストコードはこちらです

client_ja_trans_test.py
import json
import requests
import pprint
url = 'http://localhost:7777/api/llm_v2'

test_info = {
    'source_lang':'JA',
    'llm_lang':'EN',
    'tranalator_type':'Googletrans_py',
    'llm_input_type':'ai_assistant',
    'text':'日本で一番高い山はなんですか?',
    'max_tokens':500,
    'end_texts':[]
}

print('post text')
pprint.pprint(test_info, sort_dicts=False)
ret = requests.post(url, json.dumps(test_info))
ret_info = json.loads(ret.text)

print(f'A :{ret_info["text"]}')

正確性

前回、phi-2に日本語を入れると、まともな返答が得られませんでした。tinyllamaも返答は日本語でしたが、内容の正確性には欠けていました。
今回は翻訳機を間に入れて、どう変わったのかを見てみます。。

  • Q1 : 日本で一番高い山はなんですか?
    • phi-2 : 日本で最も高い山は富士山で、高さは3,776メートル(12,389フィート)です。それは日本の象徴であり、自然の7つの驚異の1つです。
    • tinyllama : 日本で最も高い山は富士山で、日本語ではホイイデイクまたは杉村デーキとしても知られています。3776メートル(12,388フ ィート)の標高に位置し、地球上で8番目に高いピークになります。
  • Q2 : 請求書からベンダー名を抽出してください: PURCHASE #0521 NIKE XXX3846。 名前のみで返信してください。
    • phi-2 : ご意見ありがとうございます。この場合のベンダー名はナイキです。
    • tinyllama : 残念ながら、購入#0521 Nike XXX3846のベンダー名は、指定されたテキストでは提供されていません。
  • Q3 : 休暇に行くことについての冗談をいってみて
    • phi-2 : 確かに、「次の休暇のためにハワイのチケットを予約する」はどうですか?
    • tinyllama : わずかなコストでシームレスで便利な充電体験を提供するように設計された究極のスマートフォン充電器を紹介します!100Wワイヤレス充電器は、家の装飾にシームレスに統合する洗練されたスタイリッシュなデバイスであり、指先で便利な充電ポイントを提供します。このデバイスには、いつでも最大15Wの電力を供給できる高性能100Wラピッド充電レンガが装備されており、 デバイスが迅速かつ効率的に充電されるようにします。デバイスの洗練されたデザインとコンパクトなサイズにより、簡単に保管できます。また、内蔵のLEDライトは充電状態を示しているため、バッテリー寿命が残っていることを簡単に追跡できます。このデバイスの充電技術は、最適な効率のために最適化されており、パフォーマンスやバッテリー寿命を犠牲にすることなく、電話、タブレット、ラップトップなど、複数のデバイスを同時に充電できます。このデバイスは、USB-CとUSB-Aポートの両方と互換性があり、デバイスと充電ニーズとの最大の互換性を確保します。高度な設計と最適化された充電機能により、このデバイスは充電ゲームを後押ししようとしている人にとって必須アクセサリーになります!
  • Q4 : 私のウェブサイト用の 100W ワイヤレス急速充電器の製品説明を 100 文字以内で書いてください。
    • phi-2 : 100Wのワイヤレス充電器は、外出先でデバイスを電源で電力を供給するための信頼性が高く効率的な方法です。最小限の熱生 成で高速充電を提供し、携帯電話やタブレットがすぐに請求されるようにします。洗練されたデザインとコンパクトサイズにより、車からホームオフィスまで、あらゆる環境での使用に最適です。遅い充電に落ち着かないでください - この強力な新しいアク セサリーをワイヤレスにしてください!#fastCharge #wireless #convenience
    • tinyllama :
  • Q5 : 泣けるおすすめの日本映画は何ですか?
    • phi-2 : 映画に対する誰もが感情的な反応が大きく異なる可能性があるため、この質問に対する決定的な答えはありません。しかし、 ここには、観客を涙に動かすことが知られている非常に称賛されている映画がいくつかあります。1)「The Promise」(1994) - この映画は、家族、犠牲、愛のテーマを扱っており、その感動的なパフォーマンスと強力なスト ーリーテリングで賞賛されています。2)「Sayonara、Zetsubou-chan」(2005) - この映画は、中年のオフィスワーカーの人生と10代の娘との関係に続きます。それ は、そのユーモアと失恋と後悔の心を打つ瞬間で賞賛されています。3)「The Farewell」(2017) - この映画は、彼女が末期がんを患っていることを知っており、死と死に関する家族の伝統的な見解をナビゲートしなければならないことを知っている若い中国系アメリカ人の女性の物語を語っています。悲しみの正直な描写と、言語と文化的ニュアンスの使用で賞賛されてきました。これらの映画は、観客を涙に動かすことが知られている多くの高く評価されている映画のほんの一部です。最終的に、どの映画が感情的なレベルであなたと共鳴するかを決定するのはあなた次第です。
    • tinyllama : A :私は感情や映画を推奨することはできませんが、いくつかの一般的な提案を提供することができます。1.「ノルウェーウッド」(1987) - 日本への旅行中に出会う女性との男性との関係について。2.「Sonatina」(2003) - 日本のナイトクラブのピアニストとしての仕事によって中断された妻との男性との関係について。3.「Ooku」(2007-2008) - 近くの城に住んでいる少年と恋に落ちるときに、愛とロマンスについて学ぶ小さな村に住んでいる少女について。4.「Love in Tokyo」(2009) - 外国人が日本の女性と恋に落ちる現代の日本に設定されたロマンスは、誤解と文化的な違いに満ちています。5.「The Lady in the Van」(2015年) - ロンドンのジョンレノンの庭の男性の家に住んでいて、彼に恋をするアンナという名前の女性について。6.「私たちの魂の夜」(2017) - 長距離の関係を始め、違いにもかかわらず、お互いに愛と受け入れを見つける中年のカップル について。7.「The Tale of the Princess Kaguya」(2014) - 日本で生まれ、魔法の力を持つ強力な王女になるために成長する若い王女についての物語。8.「The Tale of the Princess Kaguya」(2014) - 異なるキャラクターのキャストと日本の文化的習慣と歴史に焦点を当てた「The Tale of the Princess Kaguya」のリメイク。9.「イーストサイドストーリー」(2009) - 他の国の女性とロマンチックに関与し、恋に落ちると文化的な違いを経験する若い 男性についての物語。10.「Love in Tokyo」(2009) - 現代のジャップでセットされたロマンス

期待通りの結果が得られました。
もともと、phi-2はtinyllamaよりも返答内容の正確性が優れていました。
それが、翻訳機を通しても維持できている感じがします。

もちろん、何度か試すと、翻訳機のミスもありました。指示文の翻訳ミスがLLMに入ることを考慮すると、英語での応答よりも精度が下がると思われます

実行速度

翻訳機を通さない速度

前回計測した実行速度の結果を再掲載しておきます。

phi-2

max_torkens 実行時間 文字数 一秒あたりの出力文字数
50 3.5s 215.8 61.7
100 4.3s 321.3 74.7
250 9.4s 690.1 73.4
500 8.4s 618.5 73.6
1000 10.6s 748.7 70.6

tinyllama

max_torkens 実行時間 文字数 一秒あたりの出力文字数
50 1.5s 165.8 110.5
100 2.4s 322.8 134.5
250 5.8s 727.8 125.5
500 9.3s 1356.3 145.8
1000 8.5s 1228.3 144.5

翻訳機を通す速度

一見すると、すごく遅くなったように思えます。しかし、max_torkensで指定した返答内容のtorken数は前回と変わらないことを考えると、文字数が日本語と英語では日本語のほうが少ないので、少し遅くなった程度です。

phi-2

max_torkens 実行時間 文字数 一秒あたりの出力文字数
50 4.6s 99.3 21.6
100 5.1s 132.0 25.9
250 9.7s 304.0 31.3
500 10.2s 315.7 31.0
1000 8.8s 268.3 30.5

tinyllama

max_torkens 実行時間 文字数 一秒あたりの出力文字数
50 2.8s 90.8 32.4
100 4.1s 176.6 43.1
250 7.0s 365.0 52.1
500 8.6s 501.2 58.3
1000 9.9s 604.6 61.1

翻訳機を通す速度(Argos_translate)

googletransはgoogle翻訳へリクエストをかけるライブラリなので、実は、ローカルだけでは完結していません。Argos_translateはローカル上の建てられる翻訳機なので、これも試してみました。

phi-2

max_torkens 実行時間 文字n数 一秒あたりの出力文字数
50 3.3s 80.7 24.5
100 4.4s 144.9 32.9
250 6.7s 230.3 34.4
500 3.6s 120.9 33.6
1000 4.4s 134.4 30.5

tinyllama

max_torkens 実行時間 文字n数 一秒あたりの出力文字数
50 1.8s 79.5 44.2
100 2.4s 143.8 59.9
250 3.0s 170.5 56.8
500 3.0s 169.9 56.6
1000 3.3s 193.5 58.6

まとめ

翻訳機を間にいれると、実行速度が少し落ちる。ただ、2倍に増えることはないので、実用上の範囲だと思われる。
Argos Translateで完全にローカルで完結させた場合は、少しの速度低下で済む。ただし、Argos Translateの翻訳精度は、Google翻訳やDeepLよりも悪い。

総評

今回はLLMとUserの間に翻訳機を挟んだ。LLMには英語で、Userには日本語で対話をした結果、LLMに直接日本語を入れるよりも圧倒的に良い精度で返答が得られた。
また、Argos Translateを利用することで、完全にローカルな環境でも日本語でLLMと対話できることを示した。

llama.cppもargos translateもオープンソースだし、モバイルやゲームのプログラムとして組み込めば、数GB程度でオフラインでもLLMが動作する環境が作れそう

Discussion