Windows環境でHuggingFaceのモデルダウンロードが途中で停止する問題の対処【備忘録】

お疲れ様です。 ライブラリを介してモデルをダウンロードする際、ダウンロード処理が途中で停止する現象が発生することがあります。 今回は、この問題の現象と、OSの設定を変更せずに回避するための手動ダウンロード手順についてまとめます。

発生した問題

Windows環境でhuggingface_hubライブラリを使ってモデルをダウンロードすると、途中で停止する現象が起こります。 特にエラー等が出るわけではなく、またフリーズするというわけでもなく、下図のような途中の状態で止まります。 Ctrl+Cで停止することはできます。

Linux(WSL)環境では問題が無かったのでWindows特有の問題のようです。

download

# Target Model
processor = AutoProcessor.from_pretrained(TARGET_MODEL_ID)
target_model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-E2B-it",
    dtype="auto",
    device_map="auto",

)

# Assistant Model (the drafter)
assistant_model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-E2B-it-assistant",
    dtype="auto",
    device_map="auto",
)

対処方法

Windows側のシステム設定やネットワークプロキシ、シンボリックリンクの設定などを変更することでライブラリを使ってのダウンロードができるようにすることも可能なようです。 ですが、あまり設定はいじりたくない場合あると思います。(業務PCとかなら尚更)

そんなときの簡単な対処方法として、ライブラリを介さずHuggingFaceから直接モデルをダウンロード(clone)する方法があります。 ダウンロードしたフォルダをコード側で指定すれば、モデルをそこから読み込み使用することができます。

HuggingFaceのモデルページ自体がGitリポジトリになっているのでgit cloneでダウンロードすることができます。 Gemma4 MTP Draftersなので、assistant modelも同じ方法でcloneしておきます。

huggingface.co

git clone https://huggingface.co/google/gemma-4-E2B-it

target_model

assistant_model

あとはプログラム側のモデル指定の部分でcloneしたフォルダを指定すればOKです。

# Target Model
processor = AutoProcessor.from_pretrained("./gemma-4-E2B-it")
target_model = AutoModelForCausalLM.from_pretrained(
    "./gemma-4-E2B-it", # cloneしたフォルダを指定
    dtype="auto",
    device_map="auto",

)

# Assistant Model (the drafter)
assistant_model = AutoModelForCausalLM.from_pretrained(
    "./gemma-4-E2B-it-assistant", # cloneしたフォルダを指定
    dtype="auto",
    device_map="auto",
)

出力もこのように問題なくできました。(簡易出力ですが。)

output

※注意: Git LFSの事前準備が必要です

大容量の重みファイルなどを正しくダウンロードするには、ローカル環境にGit LFSがインストールされている必要があります。 インストールされていない状態でgit cloneを行うと、大容量ファイルのポインタファイル(数KBのテキスト)のみがダウンロードされます。 これはモデルファイルとは別物なので読み込み時にエラーが発生します。

Windowsの場合は、Git for Windowsでインストールをしているとデフォルトでインストールされます。 今回はWindows環境を想定しているので問題にはならないはずです。
下記のコマンドでインストールされているか確認できます。

  • インストール確認のコマンド

git lfs version

git_lfs

Gemma4 MTP DraftersをHuggingFaceから試す

お疲れ様です。

先月Gemma4の発表に続き、Gemma4 MTP Draftersがリリースされました。 今回はこちらを試していきたいと思います。

先月のGemma4:E4Bを試したときの記事も併せてどうぞ。

fallpoke-tech.hatenadiary.jp

Gemma4 MTP Drafters とは

ざっくりと説明すると、Gemma4のモデルに加えて補助用のモデルを用いて高速化したものです。 具体的な仕組みについては以下Google公式のリリースや解説記事を参照ください。

blog.google

note.com

実装

ソースコードは例によってこちらの検証環境のGitHubリポジトリに残してあります。

github.com

今回のモデル(というか手法?)はOllamaで使うことができなかったので、HuggingFaceで公開されているモデルを利用します。

ソースコードは以下になります。リポジトリ内の"chat/generate_response_gemma4.py"です。
注目すべきはTarget ModelとAssistant Modelを個別に呼び出している部分です。 Target Modelで返答を生成するときにAssistant Modelを引数で設定しています。

2つのモデルを使用しているせいか、少しGPUメモリの使用量は増えているように思います。 前回まででGemma4:E4Bを使用していましたそちらでは私の実行環境のGPU(RTX4060ti 16GB)に乗り切らなかったのでそれより軽量のモデルであるGemma4:E2Bの方を使用しています。

# Target Model
processor = AutoProcessor.from_pretrained("google/gemma-4-E2B-it")
target_model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-E2B-it",
    dtype="auto",
    device_map="auto",
)

# Assistant Model (the drafter)
assistant_model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-E2B-it-assistant",
    dtype="auto",
    device_map="auto",
)

def response_generator_langchain_gemma4_rag() -> str:
    """langchain_huggingfaceを使用+RAG(Gemma4用)
    """    
    # 直前のユーザの入力を取得
    user_input = st.session_state.messages[-1]["content"]
    
    with open("./biography_context.txt", mode="r", encoding="utf-8") as f:
        context = f.read()
    
    # システムプロンプトの用意
    system_prompt = [{
        "role": "system",
        "content": "あなたはユーザの質問に答えるアシスタントです。"
    }]
    
    # 直前のユーザの入力を取得
    rag_input = [{
        "role": "user",
        "content": RAG_PROMPT.format(question=user_input, context=context)
    }]
    
    # Process input
    text = processor.apply_chat_template(
        system_prompt + st.session_state.messages[:-1] + rag_input, 
        tokenize=False, 
        add_generation_prompt=True, 
    )
    inputs = processor(text=text, return_tensors="pt").to(target_model.device)
    input_len = inputs["input_ids"].shape[-1]
    
    # Generate output
    outputs = target_model.generate(
        **inputs,
        assistant_model=assistant_model,
        max_new_tokens=1024,
    )
    response = processor.decode(outputs[0][input_len:], skip_special_tokens=False)
    
    # Parse output
    parsed_response = processor.parse_response(response)
    
    response_html = Markdown().convert(parsed_response["content"])
    
    return response_html

実行

上記のコードを使用して実行しました。
RAGのコードになっており、コンテキストはエレファントカシマシの公式サイトのBiographyの内容をすべて与えています。

出力結果は以下のようになりました。
モデルとしてはGemma4シリーズで一番小さいですがいい感じの出力になっていると思います。 やっぱりGemma4シリーズは日本語の性能が高いように感じますね。

output

注目の実行時間ですが、なんと5秒ほどでした。
モデルサイズや実行環境など条件が異なるのですが、前回Gemma4:E4Bを試した時は最大で45秒ほどかかっていたのでなかなかに衝撃でした。

ローカルLLMでもここまでの速さで精度の良い出力できるようになったのはすごいですね。

elapsed_time

FastAPIのDependsについて知る【備忘録】

お疲れ様です。

FastAPIで使用するDependsという機能について調べたまとめです。 公式ドキュメントやAI等を使って勉強はしましたが、すべて理解できた気がしないので一旦は理解できた範囲でメモを残しておこうと思います…。

fastapi.tiangolo.com

Dependsとは

FastAPIのエンドポイントで依存関係(Dependency Injection) を定義するための仕組みのこと。

具体的には下記のような処理で使用できます。 ユーザ認証用の処理でよく使用されているイメージですね。

  • DBセッション取得
  • 認証・認可
  • Logger取得
  • 設定値取得
  • 共通バリデーション
  • 共通クエリパラメータ処理

Dependency Injectionとは

依存性注入(DI: Dependency Injection)は、「必要なオブジェクトを自分で作らず、外部から渡してもらう設計」のこと。
関数なら引数でオブジェクト間の依存関係を切り離した状態で渡すという設計上の考え方というイメージです。

使い方

下記のような形で書きます。(以前の記事で作成したコードを流用しています。)
例によってソースはGitHubにも残しておきました。

github.com

Depends内に記載した関数を先に実行して引数の値に注入します。 引数queryはクエリパラメータをBaseModelの形でデータが入り、引数loggerにはFastAPIのRequestから取得したAPIのURL(="/test")をロガー名で設定したロガーが作成されます。

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()

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
    
    
def get_endpoint_logger(req: Request) -> logging.Logger:
    """APIエンドポイント用のロガーを取得する関数

    Args:
        req (Request): APIエンドポイントのRequestオブジェクト

    Returns:
        logging.Logger: APIエンドポイント用のロガーのインスタンス
    """
    endpoint_name = req.url.path
    return logging.getLogger(endpoint_name)


@app.get("/test")
def base_model_test(
    query: Annotated[RequestModel, Depends()],
    logger: Annotated[logging.Logger, Depends(get_endpoint_logger)]
) -> ResponseModel:
    """getメソッドでテスト"""
    # 受け取ったリクエストモデルを表示
    logger.info(f"変換なし: {query.model_dump()}")
    logger.info(f"変換あり: {query.model_dump(by_alias=True)}")
    
    # レスポンスモデルを返す
    response = ResponseModel(
        user_id=query.user_id,
        user_name=query.user_name,
        message="getメソッドでレスポンスモデルのテスト"
    )
    
    return response

Depends内の関数の引数について

Depends内の関数の引数はFastAPIが自動解決して入力してくれるようになっているようです。 値の取得は下記のものから行います。パラメータ系やRequestはよく使いそうです。

  • クエリパラメータ
  • パスパラメータ
  • HTTP Header
  • Cookie
  • Request
  • 階層化されたDepends

Annotatedを使う書き方について

最近のFastAPIではこの書き方が推奨されています。Ruffを使っていると警告が出ます。
型ヒントがきれいに書ける点、VSCodeを使う場合など補完がうまく機能してくれたりするので基本こちらの書き方で書くのが良さそうです。

動作確認

先ほどのコードを実行してみました。 SwaggerUIからクエリパラメータを設定して実行します。

SwaggerUI

実行するとこのような出力になりました。
上で説明した通りqueryにはクエリパラメータをBaseModelがloggerにはAPIのURLを名前に設定したロガーが取得できています。

result

参考サイト

DevContainerでRuffを使うための設定をしたい【備忘録】

お疲れ様です。

最近の業務でリンターやフォーマッターを使う中でRuffが結構良いなと思ったので、個人開発の環境にも設定してみました。

Ruffとは

Rust製の高速なコードフォーマッター・リンターです。 パッケージ管理ツールのuvと同じ開発元が作成しています。 uvと同じように各種設定を1つの"pyproject.toml"内に記載できる点も便利に思っています。

docs.astral.sh

環境設定

過去に作成したuvを使用したDevContainerのテンプレートにRuffを追加しました。

github.com

インストール

"pyproject.toml"にRuffのインストール設定を追加しました。

コンテナの立ち上げ時にuv syncが走るようになっているので、このタイミングでuvによってインストールされます。 「dependency-groups」の"dev"に設定しています。 これによりコマンドによってインストールするライブラリを制御できるので、例えばリリースの時にgroup=devのライブラリを含めないようにできたりします。

[project]
name = "uv-devcontainer-template"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

[dependency-groups]
dev = [
    "ruff>=0.15.11",
]

Ruffの機能設定

上述の通り、"pyproject.toml"内にRuff本体の設定を記載します。

下記のサイトを参考に設定しました。サイト内の説明がかなり詳しいので併せてどうそ。 自身の環境や好みに応じて設定を変更するようにすると良いと思います。 zenn.dev

[tool.ruff]
line-length = 100
target-version = "py313"
exclude = [
    ".git",
    ".venv",
    "__pycache__",
]

[tool.ruff.lint]
select = [
    "E",
    "W",
    "F",
    "I",
    "C",
    "B",
    "UP",
]
ignore = ["E501", "C901", "UP015"]

[tool.ruff.lint.isort]
known-first-party = ["src"]
combine-as-imports = true

VSCodeの設定

DevContainerはVSCodeの機能なので、Ruffの導入に併せてVSCode側の設定も行いました。 こちらの設定により、pyファイルの保存をするたびにコード成形とリントチェックを自動で実行してくれます。

下記のサイトを参考にしました。併せてどうぞ。 zenn.dev

対応したこととしては、Ruffの拡張機能のインストールと"settings.json"の作成です。

まず、Ruffの拡張機能のインストールですが、"devcontainer.json"に記載してコンテナ立ち上げ時に自動でインストールされるように設定しています。 charliermarsh.ruffがRuffの拡張機能です。

{
    "name": "uv",
    "build": {
      "dockerfile": "../Dockerfile"
    },
    "customizations": {
      "vscode": {
        "settings": {
          "terminal.integrated.defaultProfile.linux": "bash"
        },
        "extensions": [
          "ms-python.python",
          "kevinrose.vsc-python-indent",
          "mhutchie.git-graph",
          "eamodio.gitlens",
          "charliermarsh.ruff"
        ]
      }
    },
    "forwardPorts": [8000],
    "remoteUser": "root",
    "runArgs": ["--gpus", "all"],
    "postCreateCommand": "uv sync --no-cache"
}

次に"settings.json"の作成です。 先ほどインストールした拡張機能をデフォルトのフォーマッター・リンターに指定し、保存時の自動適用を有効にしています。

{
    "[python]": {
        "editor.formatOnSave": true, 
        "editor.codeActionsOnSave": {
            "source.fixAll": "explicit", 
            "source.organizeImports": "explicit" 
        },
        "editor.defaultFormatter": "charliermarsh.ruff"
    },
    "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}

動作確認

ここまでの設定後にコンテナを立ち上げ、確認用のPythonコードを用意しました。 GitHub Copilotで"pyproject.toml"をコンテキストにして設定を確認するためのコードを生成してもらいました。

設定が正常に適用されているとこのようにリントエラーがある部分に黄色い波線が表示されていることがわかります。

lint_error_code

これを保存(Ctrl+S)すると、リントチェックが適用され削除可能なものは削除されます。 一部unsafeなリントエラーがありこれらは自動修正されずに残っています。 こちらは"pyproject.toml"に下記の設定を追加することで自動修正を有効にできますが、思わぬ不具合が起こる可能性があるので注意が必要です。

[tool.ruff]
unsafe-fixes = true

lint_check

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