背景や目的
数か月前に、GPT APIのVLM機能を使って空撮写真から熊の有無を判定したことがありました。
(記事はこちら:VLMで熊問題に対策)
VLMは商用の特定用途で使えそうなので、OSSモデルでどこまでできるかとても興味があり、
今回は、先週公開されたGoogleのGemma4を、追加学習せずに試してみます。
試してみる事
Gemma4を使って、画像内の対象物をどの程度検出できるかを確認します。
試作品やその評価方法については、以下の通りです。
- 検出プログラム
・画像を受理し、指定対象物(熊)を検出。
・検出結果から熊の頭数を取得。 - 利用モデル
・AIモデルとして、Gemma4の以下の4種類を使用します。
- E2B:総パラメータ数5.1B、有効パラメータ数2.3Bのモデル
- E4B:総パラメータ数8B、有効パラメータ数4.5Bのモデル
- 26B A4B:総パラメータ数25.2B、有効パラメータ数3.8Bのモデル
- 31B:総パラメータ数30.7Bのモデル
→詳細(Gemma4) - 実行環境
・Google Colab Pro+を使用。
・ランタイムはA100。 - 評価方法
・評価用画像4枚の登録。
・検出処理コードの実行。
・モデルごとの判定結果の比較。
次に進みましょう。
Gemma4利用事前準備
Gemma4を使うため、事前に以下の手順を済ませておきます。
まずHugging Face(以降、HF)でGemma4利用の承認が必要かを確認し、あわせてHFのトークンを作成します。HFトークンは、ColabからGemma4の利用・ダウンロードする際の認証に使います。
・HFにログインし、Gemma4の画面を開きます。モデルカード中央にライセンス承認欄が表示されていないことを確認します。表示がなければ、そのまま次に進みます。

参考:Gemma3利用時は、以下のようにライセンス承認欄が表示され、承認が必要でした。

・次はアクセス トークン画面にアクセスし、「+Create new token」を押します。

・トークンタイプは「Read」を選択し、名前を入力して「Create token」を押します。

・トークンの値が表示されますが、後から再表示ができないため、Colabで利用するために控えておきます。

以上で事前準備は完了です。
続いて、コード作成に進みます。
コード作成
冒頭で触れた通りGoogle Colab上で動作させます。
まずは上で準備したHFトークンは、コードに直接書かず安全に保存できるColab Secretsに設定します。
・左メニューの「シークレット」でHF_TOKENを登録し、「ノートブックからのアクセス」を有効にします。

実際のコードは以下の通りです。
# ---------------------------------------------------------
# 0) インストール install
# ---------------------------------------------------------
!pip install -U transformers torch torchvision accelerate
# ---------------------------------------------------------
# 1) Hugging Face ログイン
# ※ Colab のシークレットに HF_TOKEN を入れておく
# ---------------------------------------------------------
from google.colab import userdata
from huggingface_hub import login
hf_token = userdata.get("HF_TOKEN")
if not hf_token:
raise ValueError("HF_TOKEN が見つかりません。Colabのシークレットに HF_TOKEN を設定してください。")
login(hf_token)
# ---------------------------------------------------------
# 2) ライブラリ import
# ---------------------------------------------------------
import os
import sys
import re
import base64
import torch
import pandas as pd
from io import BytesIO
from datetime import datetime
from PIL import Image
from IPython.display import HTML, display
from transformers import AutoProcessor, AutoModelForImageTextToText
print("transformers version:", __import__("transformers").__version__)
# ---------------------------------------------------------
# 3) 入力画像のパス
# ここを書き換えてください
# ---------------------------------------------------------
image_paths = [
"/content/IMG_1_36.243291047333976_137.6538881672855_20251219_1400.png", # 山中
"/content/IMG_2_36.127453482727184_137.63021075324238_20251219_1430.png", # 里山
"/content/IMG_3_36.24678054268237_137.9549634060749_20251219_1500.png", # 市街地
"/content/IMG_3_36.24348509716091_137.97369485421552_20251219_1530.png", # 市街地
]
# ---------------------------------------------------------
# 4) 画像に対応する住所情報
# 表示用
# ---------------------------------------------------------
PATH_TO_ADDRESS = {
image_paths[0]: "長野県松本市安曇***",
image_paths[1]: "長野県松本市安曇***",
image_paths[2]: "長野県松本市中央***",
image_paths[3]: "長野県松本市深志***",
}
# ---------------------------------------------------------
# 5) モデル指定
# ---------------------------------------------------------
MODEL_ID = "google/gemma-4-E2B-it"
#google/gemma-4-E2B-it
#google/gemma-4-E4B-it
#google/gemma-4-26B-A4B-it
#google/gemma-4-31B-it
# ---------------------------------------------------------
# 6) モデル読込
# ---------------------------------------------------------
print("モデルを読み込み中...")
processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
torch_dtype="auto",
device_map="auto",
).eval()
print("モデル読み込み完了")
# ---------------------------------------------------------
# 7) ファイル名からエリア番号を取得
# 例: IMG_1_xxx -> 1
# ---------------------------------------------------------
def parse_area_no(path: str):
name = os.path.basename(path)
m = re.match(r"IMG_(\d+)_", name)
return int(m.group(1)) if m else None
# ---------------------------------------------------------
# 8) ファイル名から緯度・経度・時刻文字列を取得
# ---------------------------------------------------------
def parse_gps_and_time(path: str):
name = os.path.splitext(os.path.basename(path))[0]
m = re.search(r"IMG_(\d+)_(-?\d+\.\d+)_(-?\d+\.\d+)_(\d{8})_(\d{4})", name)
if not m:
return None, None, None, name
area_no = int(m.group(1))
lat = float(m.group(2))
lon = float(m.group(3))
time_str = f"{m.group(4)}_{m.group(5)}"
return area_no, lat, lon, time_str
# ---------------------------------------------------------
# 9) エリア番号をラベル化
# ---------------------------------------------------------
def classify_location_by_area_no(area_no: int) -> str:
if area_no == 1:
return "山中"
elif area_no == 2:
return "里山"
elif area_no == 3:
return "市街地"
else:
return "不明"
# ---------------------------------------------------------
# 10) Gemma 4 に熊の頭数を推論部
# 返り値:
# 0 -> 熊なし
# 1以上 -> 熊あり
# None -> 判定不能
# ---------------------------------------------------------
def ask_bear_count(image_path: str):
image = Image.open(image_path).convert("RGB")
system_prompt = (
"あなたは野生動物監視の判定アシスタントです。"
"与えられた画像を見て、熊の頭数だけを厳密に判定してください。"
)
user_prompt = """
出力ルール:
- 出力は数字1つだけ
- 熊がいない: 0
- 熊がいる: 1以上の整数(頭数)
- 小さすぎる、暗い、遠すぎる、不鮮明などで判定不能: -1
注意:
- 犬、人、木、岩、影、看板を熊として数えない
- 同じ熊を二重カウントしない
- 自信が低い場合は -1
""".strip()
messages = [
{
"role": "system",
"content": [{"type": "text", "text": system_prompt}],
},
{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": user_prompt},
],
},
]
inputs = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
)
inputs = {
k: v.to(model.device) if hasattr(v, "to") else v
for k, v in inputs.items()
}
input_len = inputs["input_ids"].shape[-1]
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=16,
do_sample=False,
)
generated = outputs[0][input_len:]
text = processor.decode(generated, skip_special_tokens=True).strip()
print(f"[DEBUG] {os.path.basename(image_path)} -> {text}")
m = re.search(r"-?\d+", text)
if not m:
return None
n = int(m.group(0))
if n == -1:
return None
return max(0, n)
# ---------------------------------------------------------
# 11) 日時文字列を見やすく整形
# ---------------------------------------------------------
def filename_to_datetime_str(time_str: str) -> str:
try:
dt = datetime.strptime(time_str, "%Y%m%d_%H%M")
return dt.strftime("%Y年%m月%d日 %H時%M分")
except:
return time_str
# ---------------------------------------------------------
# 12) サムネイル画像を HTML 化
# ---------------------------------------------------------
def make_thumbnail_html(image_path: str, max_w=180):
if not image_path or not os.path.exists(image_path):
return "<div style='color:#999'>画像なし</div>"
img = Image.open(image_path).convert("RGB")
w, h = img.size
scale = max_w / float(w)
img = img.resize((max_w, int(h * scale)))
buf = BytesIO()
img.save(buf, format="JPEG", quality=85)
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
return (
f"<img src='data:image/jpeg;base64,{b64}' "
f"style='width:{max_w}px; border-radius:10px; "
f"box-shadow:0 2px 10px rgba(0,0,0,0.15)'>"
)
# ---------------------------------------------------------
# 13) 推論実行
# ---------------------------------------------------------
rows = []
for path in image_paths:
area_no, lat, lon, time_str = parse_gps_and_time(path)
location = classify_location_by_area_no(area_no) if area_no is not None else "不明"
result = "不明"
count = "-"
if not os.path.exists(path):
result = "エラー"
count = "-"
else:
c = ask_bear_count(path)
if c is None:
result = "不明"
count = "-"
elif c == 0:
result = "安全"
count = "-"
else:
result = "注意"
count = c
rows.append({
"結果": result,
"頭数": count,
"エリア": location,
"時刻": time_str,
"住所": PATH_TO_ADDRESS.get(path, "長野県松本市"),
"image_path": path,
})
# ---------------------------------------------------------
# 14) DataFrame 作成
# ---------------------------------------------------------
df = pd.DataFrame(rows)
df.index = range(1, len(df) + 1)
df2 = df.copy()
df2["詳細"] = df2.apply(
lambda r: f'{r["住所"]} / {filename_to_datetime_str(str(r["時刻"]))}',
axis=1
)
df2["サムネ"] = df2.apply(
lambda r: make_thumbnail_html(r["image_path"]),
axis=1
)
df_view = df2[["エリア", "結果", "頭数", "詳細", "サムネ"]]
# ---------------------------------------------------------
# 15) 表示用の色付け
# ---------------------------------------------------------
def style_result_cell(val):
if val == "注意":
return "background-color:#ff4d4d;color:white;font-weight:800;text-align:center;"
if val == "安全":
return "background-color:#2ecc71;color:white;font-weight:800;text-align:center;"
return "background-color:#bdc3c7;color:white;font-weight:800;text-align:center;"
styled = (
df_view.style
.map(style_result_cell, subset=["結果"])
.set_properties(**{
"font-size": "18px",
"padding": "14px",
"border": "1px solid #e5e7eb",
"vertical-align": "middle",
})
.set_properties(subset=["頭数"], **{
"text-align": "center",
})
.set_table_styles([
{
"selector": "th",
"props": [
("font-size", "18px"),
("font-weight", "800"),
("background-color", "#111827"),
("color", "white"),
("padding", "14px"),
("text-align", "left"),
]
},
{
"selector": "table",
"props": [
("border-collapse", "separate"),
("border-spacing", "0"),
("border-radius", "14px"),
("overflow", "hidden"),
("box-shadow", "0 10px 30px rgba(0,0,0,0.12)"),
]
},
])
)
display(HTML(styled.to_html(escape=False)))
print("\n=== 判定結果の元データ ===")
display(df)モデル指定箇所は、上記コードの65行目です。まず 「MODEL_ID = “google/gemma-4-E2B-it”」 と設定し、その後はこの部分のみを置き換えて試します。
設定したモデルへ推論部は134行目~205行目です。そこで systemとuserのプロンプトを定義してモデルに推論させます。
参照する4つの画像ファイルは以下の通りです。実行前にColabへアップしておきます。




(注1)画像は実際に撮影したものではありません。実験用のダミー画像です。
・Colab画面左側の「Files」にドラッグ&ドロップしてアップロードします。

その後、アップした各画像ファイルのパスを、上記のコードの42行目の「image_paths」に設定します。
さて、実行してみましょう!
実行結果
順番にモデルを変更して、上記のコードセルを実行しました。
結果は以下の通りです。(セル内数値:対象物(熊)の発見数)
| 画像↓モデル→ | E2B | E4B | 26B A4B | 31B |
|---|---|---|---|---|
| No.1 (正解:2) | 2 | 2 | 2 | 2 |
| No.2 (正解:1) | 0 | 0 | 1 | 1 |
| No.3 (正解:1) | 1 | 1 | 1 | 1 |
| No.4 (正解:0) | 0 | 0 | 0 | 0 |
| 評価 | △ (No.2で熊を発見できなかった) | △ (No.2で熊を発見できなかった) | 〇 | 〇 |
E2BとE4B(小モデル)は、熊の有無について1件のみ不正解で、他は正解でした。
一方、26B A4Bと31B(大モデル)は、熊の有無・頭数ともに全件正解で完璧でした。前回のGPT APIの結果と同等であり、OSSで有力な候補といえそうです。
ちなみに、処理時間は以下の通りでした。(大半はモデルのロード時間です。ロード後の推論は比較的短時間でした)
・E2B:55秒
・E4B:1分29秒
・26B A4B:3分36秒
・31B:3分32秒
まとめ
OSSモデルであるGemma4のVLM機能を試してみました。
小モデルは正答率75%でしたが、大モデルでは正答率100%で、良い検出結果が得られました。
OSSモデルでもGPTと同じ精度を得られそう、と分かった点が今回の成果でした。
精度以外では、モデルのロード時間や、ツール化するためにはGPU動作環境を別途用意する必要がある点など、が気になりました。
次は、追加学習も視野に入れ、特定用途での活用検討に進みたいと思います。
OSSದು ಹೆಚ್ಚಿನ ಉಪಯೋಗಗಳನ್ನು ಅನ್ವೇಷಿಸೋಣ
(OSSモデルの使用をもっと見つけましょう)
