Loading
BLOG 開発者ブログ

2025年8月13日

オンプレLLM導入の現実解:GPUゼロでも動く日本語LLMをJMMLUで評価

生成AIは、ここ数年で急速に普及し、プロトタイピングから本番運用までのリードタイムを劇的に短縮してきました。
しかし多くのプロジェクトでは、クラウドリソースを利用することが前提となっており、よりセキュアな環境では導入をためらうケースが少なくありません。

そこで本記事では、GPUを一切使わず、CPUと十分なRAMだけで大規模言語モデル(LLM)をどこまで実用レベルで動かせるのか、LLMのベンチマーク手法の紹介もしつつ検証していきます。

 

目次

 

はじめに

こんにちは。クラウドソリューショングループのwatanabe.tです。
ここ数年で生成AIは業務に欠かせないものになりつつあり、様々な職種で使われるようになってきました。
また、LLM自体も数多くの企業が研究・開発し、日夜性能の向上が図られています。

では、実際に業務に生成AIを組み込もうと思ったとき、どのLLMを利用するのが良いのか、迷ってしまうことがあると思います。
実際には利用する際のアプリケーションの使いやすさに依存する部分も大きくありますが、そもそものLLMの性能が高いものを利用するに越したことはありません。
そこで、本記事ではLLMのベンチマーク手法について解説し、いくつかのオンプレLLMでの計測結果を踏まえ、皆さんが適切なモデル選択をするお手伝いをしていきたいと思います。

LLMのベンチマークについて

LLMのベンチマークと一言でいっても、「どのタスクを、どの手順で、どの指標を測るか」によって結論が大きく変わります。
(要は、生成AIに実行させたいタスクの性質によって、向き・不向きが変わるということ)
調べ物をしたいのか、プログラムを書かせたいのか、英語で問題ないのか、日本語が良いのか、それらによっても採るべき評価アプローチが変わります。

その中でも今回は、MMLU(Massive Multitask Language Understanding)を基にした、JMMLU(Japanese Massive Multitask Language Understanding Benchmark)というベンチマークを採用して計測を行います。
MMLUはゼロショット・多肢選択式でモデルの知識・推論力を測り、基礎学術から常識まで広く網羅しています。
JMMLUはMMLUを日本語対応させつつ、日本特有の知識などを追加したベンチマークになっており、日本語での論理的な考え方や知識の網羅性を確認することができます。
このスコアの高低によって、日本語でモデルを利用したときのハルシネーションの発生具合などを見ていきましょう。

測定環境

本検証ではGPUを一切使わず、以下のCPUとRAMだけで性能測定を行いました。

  • OS: Windows 10 Enterprise (22H2)
  • CPU: 13th Gen Intel(R) Core(TM) i5-1335U 1.30 GHz
  • RAM: 16.0 GB

 

対象モデル・データ

本検証で利用したモデルは以下の通りです。(マシンスペックの制約から、あまり高性能なモデルは利用できませんでした……)

  • meta-llama/Llama-3.2-3B-Instruct
  • elyza/Llama-3-ELYZA-JP-8B
  • neoai-inc/Llama-3-neoAI-8B-Chat-v0.1
  • Qwen/Qwen3-8B

また、本検証で利用したテストデータは以下の通りです。

  • computer_security(コンピュータセキュリティ)
  • japanese_idiom(日本の慣用句)
  • japanese_geography(日本の地理)

 

計測手順・プログラム

テストデータは全てCSV形式で、左から「問題」「選択肢A」「選択肢B」「選択肢C」「選択肢D」「正解」となっています。
ここからそれぞれの行ごとに問題と選択肢を抽出し、プロンプトに投入してLLMに質問し、回答が「正解」と一致するかを確認していきます。

今回のプロンプトはシンプルに、以下のように与えることにしました。

You are an expert exam-taker.
Read the multiple-choice question and reply **ONLY** with the SINGLE letter
(A, B, C, or D) that correctly completes it.

Question:
{question}

A. {A}
B. {B}
C. {C}
D. {D}

Answer:

このプロンプトでの質問やCSVからの読み込みなどを自動化するため、以下のようなプログラムで計測は実施しました。
バッチ的に並列推論もできるように作りましたが、スペック的に1並列(つまり直列)が限界でした……

#!/usr/bin/env python3
"""
main.py

MMLU 形式 (question, A, B, C, D, answer) の CSV を読み込み、
Hugging Face Transformers の pipeline でゼロショット回答を行い精度を計測する。

CSV 必須列: question, A, B, C, D, answer
"""

from __future__ import annotations
import argparse
import re
import time
from pathlib import Path
from typing import List

import pandas as pd
from tqdm import tqdm
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    pipeline,
    Pipeline,
)


# ---------- Prompt ----------
PROMPT_TEMPLATE = """You are an expert exam-taker.
Read the multiple-choice question and reply **ONLY** with the SINGLE letter
(A, B, C, or D) that correctly completes it.

Question:
{question}

A. {A}
B. {B}
C. {C}
D. {D}

Answer:"""
# -----------------------------

LETTER_RE = re.compile(r"[A-D]")

def extract_letter(text: str) -> str:
    """LLM 出力から最初に現れる A–D を抽出"""
    m = LETTER_RE.search(text.upper())
    return m.group(0) if m else ""


def build_pipeline(
    model_name: str,
    device: str | int | None,
    max_new_tokens: int,
    **model_kwargs,
) -> Pipeline:
    """
    HuggingFace からモデルとトークナイザをロードして text-generation pipeline を生成
    device: "cpu", "cuda", "mps", or int (GPU ID). None なら auto
    model_kwargs: torch_dtype, trust_remote_code など自由に渡せる
    """
    tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True, **model_kwargs)
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        device_map="auto" if device is None else None,
        **model_kwargs,
    )
    model.generation_config.pad_token_id = tokenizer.eos_token_id  # パディングトークンを設定
    return pipeline(
        "text-generation",
        model=model,
        tokenizer=tokenizer,
        device=device,
        max_new_tokens=max_new_tokens,
        do_sample=False,      # 複数の回答候補を生成しない
        temperature=0.0,
        return_full_text=False,
    )


def batch_iter(lst: List[str], n: int):
    """n 個ずつのバッチイテレータ"""
    for i in range(0, len(lst), n):
        yield lst[i : i + n]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("csv_path", help="問題 CSV パス")
    parser.add_argument(
        "--model",
        required=True,
        help="Hugging Face Model ID or local path (例: meta-llama/Meta-Llama-3-8B-Instruct)",
    )
    parser.add_argument(
        "--device",
        default=None,
        help='"cpu", "cuda", "mps" もしくは GPU ID。未指定なら自動',
    )
    parser.add_argument("--batch-size", type=int, default=4, help="推論バッチサイズ")
    parser.add_argument("--max-new-tokens", type=int, default=1, help="生成トークン数上限")
    parser.add_argument(
        "--trust-remote-code",
        action="store_true",
        help="モデルのカスタムコードを信頼する (必要な場合のみ)",
    )
    args = parser.parse_args()

    # CSV 読み込み & 検証
    df = pd.read_csv(args.csv_path)
    required_cols = {"question", "A", "B", "C", "D", "answer"}
    if not required_cols.issubset(df.columns):
        raise ValueError(f"CSV must contain columns: {required_cols}")

    # モデルロード
    print("Loading model …")
    pipe = build_pipeline(
        model_name=args.model,
        device=args.device,
        max_new_tokens=args.max_new_tokens,
        trust_remote_code=args.trust_remote_code,
    )

    prompts = [
        PROMPT_TEMPLATE.format(
            question=row["question"],
            A=row["A"],
            B=row["B"],
            C=row["C"],
            D=row["D"],
        )
        for _, row in df.iterrows()
    ]

    predictions, durations = [], []
    print("Running inference …")
    for batch in tqdm(batch_iter(prompts, args.batch_size), total=(len(prompts) + args.batch_size - 1) // args.batch_size):
        t0 = time.perf_counter()
        outputs = pipe(batch)
        durations.extend([time.perf_counter() - t0] * len(batch))
        for out in outputs:
            # pipeline の戻り値は list[dict] or str depending on version
            text = out[0]["generated_text"] if isinstance(out, list) else out
            predictions.append(extract_letter(text))

    # 結果集計
    df["prediction"] = predictions
    df["correct"] = df["prediction"].str.upper() == df["answer"].str.upper()
    df["seconds"] = durations

    accuracy = df["correct"].mean()
    print(f"\nAccuracy: {accuracy:.2%} ({df['correct'].sum()}/{len(df)})")
    print(f"Avg latency: {df['seconds'].mean():.2f} s / question (batched)")

    model_name = args.model.split("/")[-1]  # 最後の部分をモデル名とする
    out_path = Path("./results/").with_stem(Path(args.csv_path).stem + "_results-" + model_name)
    df.to_csv(out_path.with_suffix(".csv"), index=False)
    print(f"Detailed results saved to {out_path}.csv")


if __name__ == "__main__":
    main()

 

ベンチマーク結果

実際の計測結果は以下の通りです。
実行時間などはその時の環境やスペックによって左右されるため、正答率のみ記載します。

テストデータ Llama-3.2-3B-Instruct Llama-3-ELYZA-JP-8B Llama-3-neoAI-8B-Chat-v0.1 Qwen3-8B
コンピュータセキュリティ 56.57% 61.62% 60.61% 71.72%
日本の慣用句 73.33% 92.67% 83.33% 92.00%
日本の地理 55.40% 82.01% 74.10% 73.38%

 

考察

結果を見てみると、やはり後発のQwen3が頭一つ抜けているなという印象です。
もちろん、パラメータ数が異なるモデルより高性能なのは想定内ですが、同程度のパラメータ数のモデルよりも全体的に高スコアを示しています。

Qwen3は中国のAlibabaが開発したLLMなので、中国語や英語に対しては高スコアになるだろうと思っていましたが、日本語のスコアについても日本語知識をファインチューニングしたモデルを大きく上回るスコアを示しているのは非常に驚きですね。

ただ、Llama-3-ELYZA-JP-8Bも日本の慣用句・日本の地理ではQwen3よりも高いスコアを示すなど、やはり日本語能力を強化しているだけあって、日本の知識が必要な分野に関しては有用であることが分かりました。

このように、言語や地域に依存した知識のベンチマーク手法として、JMMLU(MMLU)は非常に有用であることが分かったかと思います。

おわりに

改めて、後発とはいえ同程度のパラメータ数であっても高スコアを出すことができるのは驚きでした。
(蒸留などの問題点があることは認識しつつ)低スペックPCでも利用できるモデルがあるというのは良いですね。

中国製モデルはセキュリティの点などが問題になることもありますが、株式会社アイソルートでは完全閉域網でLLMを動作させるサービスを提供しています。
社外秘情報を扱う高性能・高セキュリティLLMなど、ご興味がありましたら是非一度お問い合わせください。

オンプレ生成AI 導入支援サービス | CLOUD FLAG


watanabe.tのブログ

クラウドソリューショングループ所属

昔はモバイルアプリ開発を、今はGCP, AWS, Firebaseなどのクラウド周りの提案/開発を行っているエンジニアです。

AWS認定ソリューションアーキテクト取得しました!