********
※LINE対応チャットボット版の
「LINEチャットボット屋」
いろんなチャットボットがあります。
ぜひ、ご覧ください!
***************
***************
スマホで動く小型SVLMは現実的なのか
ここ最近、AIの話題の中でよく見かけるようになったのが、「エッジデバイスで動く小型のマルチモーダルAI」です。
特に、防衛や産業用途の記事では、ドローンや携帯端末のような現場の機器で、小型の視覚言語モデル、いわゆるSVLMを動かし、その場で状況認識や報告を行うという構想が語られることが増えてきました。
たしかに考えてみると、現場で撮影した画像や動画をその場で簡単に理解し、短い報告文に変換できるAIがあれば便利です。
通信が不安定な場所でも最低限の認識ができ、そこから上位システムへ情報を渡せるからです。
では実際に、スマホのような比較的限られた計算資源の上で、そうした小型SVLMはどこまで現実的なのでしょうか。
今回は、その感触をつかむために試験的な実験を行ってみました。
小型SVLMとは何か、なぜ注目されているのか
SVLMは、画像や動画とテキストをまとめて扱えるマルチモーダルAIの中でも、比較的小さく、軽量化を意識したモデルのことです。
大規模なVLMやLLMがクラウド上で強力な推論を行うのに対し、小型SVLMはスマホやドローン、携帯端末などのエッジ側で動かすことを視野に入れています。
この小型SVLMが注目される理由ははっきりしています。
現場では、すべてのデータを毎回クラウドに送って解析するとは限らないからです。
通信の遅延、接続不良、セキュリティ上の制約などを考えると、まず端末側で「何が見えているか」を簡潔に把握し、必要な情報だけを上位へ渡す仕組みのほうが実用的な場面が多いのです。
ただし、ここで誤解してはいけないのは、小型SVLMは「何でも考えて判断する万能AI」ではないという点です。
むしろ現実的な役割は、「見て、短く説明する」「異常候補を挙げる」「報告文のたたき台を作る」といった一次処理に近いものだと考えられます。

Qwen系モデルで動画理解を試してみた
今回の実験では、Qwen系のマルチモーダルモデルをGoogle Colab上で動かし、短い動画を読み込ませて、その内容を説明させるテストを行いました。
使ったのは3Bクラスと7Bクラスのモデルです。
Qwen2.5-VL-7B-Instruct
Qwen2.5-VL-3B-Instruct
実験の狙いは、現時点の比較的小型なマルチモーダルモデルが、動画をどこまで理解し、どの程度自然な説明や推測ができるのかを見ることにありました。
入力したのは、白い球体がテーブルの端付近に置かれている短い動画です。

人間が見ると、球体の位置関係や動きの方向、落ちそうかどうか、といった点に自然と注意が向きます。
そこで、まずは「この動画で何が起きているか」といった基本的な説明をさせ、次に「この先どうなると思うか」といった続きを推測させる質問を投げました。
この種の実験で重要なのは、単に答えが流暢かどうかではありません。
本当に映像の内容を見て答えているのか、それとも一般論で埋めているのかを見分ける必要があります。
そのため、今回の実験では、シーン説明だけでなく、位置変化や未来予測のような少し難しい問いも加えてみました。
3Bと7Bを比べて見えた違い
実際に試してみると、3Bと7Bではかなり違いがありました。
まず、7Bのほうが全体として説明文が自然で、場面のまとまり方も良かったです。
白い球体、木のテーブル、端にある状態、といった特徴を比較的きれいに拾ってくれました。
少なくとも「何が映っているか」をざっくり説明させる用途では、7Bのほうが一段上だと感じました。
一方で、未来予測や物理的な推測になると、7Bでもまだ弱さが見えました。
たとえば「このあとどうなるか」と聞くと、ボールが落ちるかもしれない、止まるかもしれない、転がるかもしれない、といった一般論の候補を並べる傾向がありました。
これは動画を厳密に解析して未来を読んでいるというより、「テーブルの端にあるボールなら、こういう可能性がある」と常識的に補っている印象です。
つまり、7Bは3Bよりも動画の雰囲気理解や説明力は高いものの、物理予測まで高精度にできるわけではありませんでした。
この違いはかなり重要で、マルチモーダルAIに何を期待するかを考えるうえで参考になります。
見たままを短く説明するのは得意でも、その先の世界の動きを正確に推論するのはまだ別の難しさがある、ということです。

スマホ実装でわかった課題
今回の実験を通して強く感じたのは、3Bでも7Bでも、そのままスマホに載せて快適に使うにはまだ重いということでした。
ColabのGPU上では動いても、スマホで同じように動画を処理し、数秒で応答を返し続けるとなると、発熱、メモリ、バッテリー、応答速度の面でかなり厳しくなります。
特に動画は重いです。
静止画1枚ならまだしも、複数フレームを処理しながらテキストを生成するとなると、見た目のパラメータ数以上に負荷を感じます。
そのため、3Bクラスをそのままスマホに持ち込んで万能に使うというよりは、もっと軽量化した1B前後のVLMに役割を限定して持たせるほうが現実的だと感じました。
ここでいう役割限定とは、「画像を見て1文で説明する」「特定対象の有無を答える」「位置の変化を短く報告する」といったものです。
逆に、「このあと何が起こるか」「なぜそうなるか」「物理的に自然か」といった深い推論は、端末側ではなく、本部側のより大きなLLMやルールベースの仕組みに任せたほうが現実的でしょう。
まとめ スマホで全部考えるより「見て報告するAI」が現実的
今回の実験を通して見えてきたのは、小型SVLMがスマホでまったく使えないわけではない、ということでした。
むしろ、「何が見えているかを短く報告する」用途なら、かなり筋がいいと感じます。
7Bクラスではシーン説明の質が高く、3Bでも方向性を見るには十分な手応えがありました。
ただし、それは「スマホで全部考えるAI」が現実的だという意味ではありません。
現時点では、スマホ側の小型SVLMは前線の目とメモ係に近い役割が向いています。
画像や動画を見て、対象物や位置変化、異常候補を短くまとめ、その結果を本部の大きなLLMや分析システムへ渡す。
この分業構成のほうが、技術的にも運用面でも自然です。
今回の実験は、そうした未来の一端を確かめるものになりました。
今後は、3Bや7Bの能力をそのまま追うよりも、必要な機能だけをうまく削り、1B前後で実用的に動かす設計がより重要になってくるのかもしれません。
今回使ったPythonコード
* 表示のバグでインテントが崩れています。
下記のコードを使用する場合は、お気に入りのAIにコピペして「インテント直して」と言ってください。
# ============================================
# Qwen2.5-VL-7B-Instruct 動画テスト 1セル版
# ボタン送信対応版
# - mp4を1回アップロード
# - 同じ動画に何回でも質問可能
# - HFアクセストークンはテキストボックス入力
# - 空欄で送信すると終了
# ============================================
!pip -q install git+https://github.com/huggingface/transformers accelerate qwen-vl-utils decord ipywidgets
import os
import gc
import time
import torch
import platform
from google.colab import files
from IPython.display import display, clear_output
import ipywidgets as widgets
# ----------------------------
# GPU確認
# ----------------------------
print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
if torch.cuda.is_available():
print("GPU:", torch.cuda.get_device_name(0))
!nvidia-smi
else:
raise RuntimeError("GPUが見つかりません。Colabの『ランタイムのタイプを変更』でGPUを選んでください。")
print("\n[注意]")
print("7B版は3B版よりかなり重いです。")
print("T4で厳しい場合は、L4/A100 か 3B版に戻すのがおすすめです。")
# ----------------------------
# HFトークン入力
# ----------------------------
print("\n=== Hugging Face アクセストークン入力 ===")
print("公開モデルなので空欄でも動くことがあります。必要なら入力してください。")
hf_token_widget = widgets.Password(
value='',
placeholder='hf_xxxxxxxxxxxxxxxxx',
description='HF Token:',
layout=widgets.Layout(width='650px')
)
display(hf_token_widget)
confirm_token_btn = widgets.Button(
description="トークン確定",
button_style='info'
)
token_status = widgets.Output()
display(confirm_token_btn, token_status)
state = {
"hf_token": "",
"token_confirmed": False,
"video_path": None,
"history": [],
"ended": False,
}
def on_confirm_token_clicked(b):
with token_status:
clear_output()
state["hf_token"] = hf_token_widget.value.strip()
if state["hf_token"]:
os.environ["HUGGINGFACE_HUB_TOKEN"] = state["hf_token"]
print("HFトークンをセットしました。")
else:
print("HFトークン未入力のまま進めます。")
state["token_confirmed"] = True
confirm_token_btn.on_click(on_confirm_token_clicked)
print("\n上の『トークン確定』ボタンを押してから次へ進んでください。")
# ----------------------------
# モデル読み込み
# ----------------------------
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
MODEL_NAME = "Qwen/Qwen2.5-VL-7B-Instruct"
dtype = torch.float16
if torch.cuda.is_available():
major, minor = torch.cuda.get_device_capability(0)
if major >= 8:
dtype = torch.bfloat16
print(f"\n=== モデル読み込み開始: {MODEL_NAME} ===")
print("dtype:", dtype)
hf_token = hf_token_widget.value.strip()
model_kwargs = {
"torch_dtype": dtype,
"device_map": "auto",
}
processor_kwargs = {}
if hf_token:
model_kwargs["token"] = hf_token
processor_kwargs["token"] = hf_token
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
MODEL_NAME,
**model_kwargs
)
processor = AutoProcessor.from_pretrained(
MODEL_NAME,
**processor_kwargs
)
print("モデル読み込み完了")
# ----------------------------
# mp4アップロード
# ----------------------------
print("\n=== mp4アップロード ===")
uploaded = files.upload()
video_path = None
for fn in uploaded.keys():
if fn.lower().endswith(".mp4"):
video_path = fn
break
if video_path is None:
raise ValueError("mp4ファイルが見つかりません。短い mp4 を1本アップロードしてください。")
state["video_path"] = video_path
print("動画ファイル:", video_path)
print("ファイルサイズ(MB):", round(os.path.getsize(video_path) / 1024 / 1024, 2))
# ----------------------------
# 推論関数
# ----------------------------
def ask_video(video_path, user_prompt, max_new_tokens=256, fps=1.0, max_pixels=360*420):
# 7Bは重いので fps と max_pixels を少し控えめにして負荷を下げる
messages = [
{
"role": "user",
"content": [
{
"type": "video",
"video": f"file://{os.path.abspath(video_path)}",
"fps": fps,
"max_pixels": max_pixels,
},
{
"type": "text",
"text": user_prompt
}
]
}
]
text = processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
text=[text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt"
)
for k, v in inputs.items():
if hasattr(v, "to"):
inputs[k] = v.to(model.device)
start = time.time()
with torch.no_grad():
generated_ids = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False
)
end = time.time()
generated_ids_trimmed = [
out_ids[len(in_ids):]
for in_ids, out_ids in zip(inputs["input_ids"], generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed,
skip_special_tokens=True,
clean_up_tokenization_spaces=False
)[0]
elapsed = end - start
return output_text, elapsed
# ----------------------------
# UI部品
# ----------------------------
print("\n=== 質問UI ===")
print("質問を入力して『送信』を押してください。")
print("空欄のまま『送信』を押すと終了します。")
prompt_widget = widgets.Textarea(
value="この動画で何が起きているか、2〜3文で日本語で説明してください。",
placeholder="ここに質問を書く。空欄で送信すると終了。",
description="Prompt:",
layout=widgets.Layout(width='900px', height='140px')
)
max_tokens_widget = widgets.IntText(
value=180,
description='MaxTokens:',
layout=widgets.Layout(width='260px')
)
fps_widget = widgets.FloatText(
value=1.0,
description='FPS:',
layout=widgets.Layout(width='220px')
)
max_pixels_widget = widgets.IntText(
value=360 * 420,
description='MaxPixels:',
layout=widgets.Layout(width='260px')
)
send_button = widgets.Button(
description="送信",
button_style='success',
icon='paper-plane'
)
end_button = widgets.Button(
description="強制終了",
button_style='warning',
icon='stop'
)
status_output = widgets.Output()
answer_output = widgets.Output()
history_output = widgets.Output()
display(prompt_widget)
display(widgets.HBox([max_tokens_widget, fps_widget, max_pixels_widget]))
display(widgets.HBox([send_button, end_button]))
display(status_output)
display(answer_output)
display(history_output)
# ----------------------------
# ボタン動作
# ----------------------------
def redraw_history():
with history_output:
clear_output()
print("=== 質問履歴 ===")
if not state["history"]:
print("まだ質問はありません。")
return
for i, item in enumerate(state["history"], 1):
print(f"\n--- {i}回目 ---")
print("Prompt:", item["prompt"])
print("Output:", item["output"])
print("推論時間:", f'{item["elapsed"]:.2f} 秒')
print("fps:", item["fps"], "| max_pixels:", item["max_pixels"])
def on_send_clicked(b):
if state["ended"]:
with status_output:
clear_output()
print("すでに終了しています。")
return
user_prompt = prompt_widget.value.strip()
max_new_tokens = int(max_tokens_widget.value)
fps = float(fps_widget.value)
max_pixels = int(max_pixels_widget.value)
if user_prompt == "":
state["ended"] = True
send_button.disabled = True
prompt_widget.disabled = True
max_tokens_widget.disabled = True
fps_widget.disabled = True
max_pixels_widget.disabled = True
with status_output:
clear_output()
print("空欄送信を検出しました。終了します。")
with answer_output:
print("\n処理を終了しました。")
return
with status_output:
clear_output()
print("推論中です... 7B版なので少し時間がかかることがあります。")
try:
output_text, elapsed = ask_video(
video_path=state["video_path"],
user_prompt=user_prompt,
max_new_tokens=max_new_tokens,
fps=fps,
max_pixels=max_pixels
)
state["history"].append({
"prompt": user_prompt,
"output": output_text,
"elapsed": elapsed,
"max_new_tokens": max_new_tokens,
"fps": fps,
"max_pixels": max_pixels,
})
with answer_output:
print("\n==============================")
print("Prompt:")
print(user_prompt)
print("------------------------------")
print("Output:")
print(output_text)
print("------------------------------")
print(f"推論時間: {elapsed:.2f} 秒")
if torch.cuda.is_available():
print(f"GPU使用メモリ allocated: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
print(f"GPU使用メモリ reserved : {torch.cuda.memory_reserved()/1024**3:.2f} GB")
print("==============================\n")
with status_output:
clear_output()
print(f"完了しました。質問回数: {len(state['history'])}")
redraw_history()
except Exception as e:
with status_output:
clear_output()
print("エラーが出ました。")
print(str(e))
print("\n対策例:")
print("- fps を 0.5 に下げる")
print("- MaxTokens を 128 に下げる")
print("- MaxPixels を 200*280 くらいに下げる")
print("- もっと短い mp4 にする")
print("- L4 / A100 に変える")
def on_end_clicked(b):
state["ended"] = True
send_button.disabled = True
prompt_widget.disabled = True
max_tokens_widget.disabled = True
fps_widget.disabled = True
max_pixels_widget.disabled = True
with status_output:
clear_output()
print("強制終了しました。")
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
send_button.on_click(on_send_clicked)
end_button.on_click(on_end_clicked)
print("\n準備完了。質問を書いて『送信』を押してください。")
****************
最近のデジタルアート作品を掲載!
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
***************




















