🐱

ローカルで完結する生成AIアプリを「日本語版 Gemma 2 2B」で実装

2024/10/13に公開


画像はDALL·E 3で生成しました

はじめに

生成AIが注目されて約2年となりますが、その活用はセキュリティやハルシネーションなどの懸念が常に付きまとってきました。特に、クライアントデータや個人情報に対して生成AIを使うことはかなりのハードルになっています。その要因の1つは、クラウド上にデータを送ることによるセキュリティリスクとなります。
例えば、この記事で取り上げる日英翻訳についても、業務情報をクラウド上にアップロードして翻訳してよいものかという議論があります。
一方で、生成AIの中でもSLM(Small Language Model)の進化はここ数か月で目覚ましいものになっている認識です。
GemmaやPhi、Llamaなど。
特に下記記事でとりあげたGemmaの日本語ファインチューニングは進んできていて、SLMでも日本語がだいぶ通用するようになってきています。
https://zenn.dev/acntechjp/articles/c870d5427a193a

そうだ、ローカル(端末のみ)で完結する翻訳アプリを作ってみよう

ここで思いついたのが、SLMを活用したローカル上で完結するアプリを作ってみてはどうかということ。
具体的には、翻訳アプリは作ってみようと思いました。

アーキテクチャ - PythonからOllama経由でGemma2へアクセス

Pythonでチャット画面を実装して、SLM「gemma-2-2b-jpn-it」へアクセスします。
SLMをローカル上で動かすので、チャットにインプットした情報がインターネットへ流出する心配はありません。

構築手順

Ollamaのインストール

下記記事はGemma2の記事ですが、Ollamaのインストール手順も記載しています。この手順を参考にインストールします。
https://zenn.dev/acntechjp/articles/026a4af5870e5f

gemma-2-2b-jpn-itのデプロイ

schronekoさんがollamaで使えるようにしてくれています。本当に感謝です。
https://ollama.com/schroneko/gemma-2-2b-jpn-it
下記コマンドでデプロイできます。

gemma-2-2b-jpn-itのデプロイ
ollama run schroneko/gemma-2-2b-jpn-it

こんなイメージで。2回目以降はデプロイではなく「gemma-2-2b-jpn-it」の実行のみとなります!

Pythonで実装

ollamaのライブラリをまずインストールしましょう。

ollamaのライブラリインストール
pip install ollama

Claudeを使ってサッとコーディングしました。

チャットボット画面
import tkinter as tk
from tkinter import scrolledtext, ttk
import ollama
import threading
import re

class OllamaChatGUI:
    def __init__(self, master):
        self.master = master
        master.title("Ollama Translator")
        master.geometry("600x500")

        self.chat_history = scrolledtext.ScrolledText(master, state='disabled', height=20)
        self.chat_history.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)

        self.direction_var = tk.StringVar()
        self.direction_var.set("English to Japanese")
        self.direction_dropdown = ttk.Combobox(master, textvariable=self.direction_var, 
                                               values=["English to Japanese", "Japanese to English"],
                                               state="readonly")
        self.direction_dropdown.pack(padx=10, pady=5)

        self.msg_entry = tk.Text(master, height=3, width=50)
        self.msg_entry.pack(side=tk.LEFT, padx=10, pady=10)
        
        self.msg_entry.bind("<Control-Return>", self.send_message)

        self.send_button = tk.Button(master, text="Translate", command=self.send_message)
        self.send_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.system_prompts = {
            "Japanese to English": """
            Translate the input message from Japanese to English.
            # Output Format
            - Provide only the translation result without any additional text or explanation.
            """,
            "English to Japanese": """
            Translate the input message from English to Japanese.
            # Output Format
            - Provide only the translation result without any additional text or explanation.
            """
        }

    def send_message(self, event=None):
        user_message = self.msg_entry.get("1.0", tk.END).strip()
        if user_message:
            direction = self.direction_var.get()
            self.append_message(f"Original ({direction.split(' to ')[0]}): " + user_message)
            self.msg_entry.delete("1.0", tk.END)
            
            threading.Thread(target=self.get_ollama_response, args=(user_message, direction)).start()

    def get_ollama_response(self, user_message, direction):
        try:
            system_prompt = self.system_prompts[direction]
            messages = [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_message}
            ]
            response = ollama.chat(model='schroneko/gemma-2-2b-jpn-it', messages=messages)
            assistant_message = response['message']['content']
            cleaned_message = self.clean_response(assistant_message)
            self.append_message(f"Translation ({direction.split(' to ')[1]}): " + cleaned_message)
        except Exception as e:
            self.append_message("Error: " + str(e))

    def clean_response(self, message):
        # 不要な文言を削除
        cleaned = re.sub(r'</?(start_of_turn|end_of_turn)>', '', message)
        # 前後の空白を削除
        cleaned = cleaned.strip()
        return cleaned

    def append_message(self, message):
        self.chat_history.configure(state='normal')
        self.chat_history.insert(tk.END, message + "\n\n")
        self.chat_history.configure(state='disabled')
        self.chat_history.see(tk.END)

if __name__ == "__main__":
    root = tk.Tk()
    gui = OllamaChatGUI(root)
    root.mainloop()

実行

先ほどのPythonをコマンドプロンプトで実行します。すると下記のようなウインドウが表示されます。

英語 → 日本語

下記英文の記事の冒頭を日本語に翻訳してみましょう。
https://openai.com/index/introducing-openai-o1-preview/
具体的には下記英文です。

翻訳する英文
We've developed a new series of AI models designed to spend more time thinking before they respond. They can reason through complex tasks and solve harder problems than previous models in science, coding, and math.

翻訳している様子はこんな感じ。

日本語 → 英語

今度は日本語から英語に翻訳してみます。
下記日本語の記事を英語に翻訳してみましょう。
https://felo.ai/ja/faq/why-felo-search
具体的には下記日本語文です。

翻訳する日本語文
Felo Search は AI 駆動の検索エンジンで、世界中の知識を発見し理解するために最適化された多言語対応の人工知能検索エンジンです。情報源を検索して詳細な回答を迅速にまとめ、ユーザーがどのような質問をしてもインターネットを検索し、理解しやすく検証可能な回答を提供します。知識を広げ、個別の洞察と情報を提供することができます。

翻訳している様子はこんな感じ。

NPU等の活用が待たれる

今回はCPUを使ったSLM活用になっています。Copilot+PCに搭載されているNPUを活用できるようになるとさらに幅が広がるのではと考えています。
実際、実装したツールを使って翻訳するとCPUが跳ね上がっていてCPUが使われていることがわかります。

おわりに

今回のアプリを応用して、メールの概要を生成する端末アプリも作れたりします。また、端末上で音声の文字お越しがスムーズにできれば通話しながら端末だけでリアルタイム通訳できる日も夢ではなくなってきています!
急速に進化を遂げている生成AI界隈ですが、引き続き追っていけたらと考えています!

Accenture Japan (有志)

Discussion