慣性聚合 関心のあるブログ、ニュース、テクノロジーを効率的に追跡
原文を読む 慣性聚合で開く

おすすめ購読元

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
PaliGemmaを使用したリアルタイムビデオ分類:低遅延VLM推論のためのアーキテクチャパターン
Pasquale Mol · 2026-05-24 · via DEV Community

以前の記事で、私たちは3つのオープンソースのビジョン言語モデルをゼロショット物体検出でベンチマークし、不快な結論に至りました:最も速い候補であるPhi-3.5-vision-instructでも、NVIDIA L4でフレームあたり4.45秒かかります。LLaVA-v1.6は8.13秒です。ライブビデオストリームを処理する必要があるどんなアプリケーションにおいても、これらの数値は不適格です。しかし、VLMsが本質的にリアルタイムワークロードと互換性がないという結論には、さらに検討が必要です。その8秒の数値は、一般的なゼロショット検出タスクで測定され、モデルに任意のシーン中の任意の物体について推論させるものでした。制約された問題ではどうなるでしょうか?モデルに閉じた語彙、固定された解像度、確定的なデコーディング戦略、非ブロッキングの推論パイプラインを与えた場合呢?
本記事はその質問に答えます。Googleのコンパクトなビジョン言語モデルであるPaliGemmaを使用して、NVIDIA RTX A4500で約0.8~1.2秒ごフレームで実行されるリアルタイムビデオ分類システムを構築しました。これは、比較可能なプロフェッショナルハードウェアにおけるLLaVAに対する6~8倍の改善であり、完全にアーキテクチャの決定を通じて、ハードウェアのアップグレードではなく達成しました。それを実現した4つのパターンはこちらです。

パリジェマはなぜ?

建築設計に入る前に、モデルの選択自体も説明に値する。なぜなら、PaliGemmaは実用的な価値に対して開発者文献における表現が著しく不足しているからだ。PaliGemmaはGoogleが構築した30億パラメータのビジョン言語モデルで、SigLIPビジョンエンコーダとGemma言語バックボーンを組み合わせている。LLaVA-7BやPhi-3.5-visionと比較すると、規模は約半分で、同じハードウェア上でのVRAM消費を直接的に低減し、推論速度も向上させる。分類タスクにおいてより重要なのは、イメージキャプション、ビジュアルクエスチョンアンサリング、オブジェクトロケーションを含む幅広いビジョン理解ベンチマークで明示的にファインチューニングされたということだ。つまり、私たちが引き出そうとするような制約的で構造的なレスポンスに対して強い事前知識を持っているということだ。

import torch
from transformers import PaliGemmaProcessor, PaliGemmaForConditionalGeneration

MODEL_PATH = "./paligemma_offline"

model = PaliGemmaForConditionalGeneration.from_pretrained(
    MODEL_PATH,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    local_files_only=True
).eval()

processor = PaliGemmaProcessor.from_pretrained(
    MODEL_PATH,
    local_files_only=True
)

フルスクリーンモードを開始 フルスクリーンモードを終了

ここで注目すべき点が二つあります。bfloat16で読み込む代わりにfloat32を使うと、分類タスクにおける精度の低下は無視できるレベルでVRAMの使用量が半分になります。local_files_only=True フラグはオフライン環境の便利さだけでなく、プロダクションシステムでは初期化時のネットワークラウンドトリップを削除し、推論環境が完全に再現可能であることを保証します.

パターン1:解像度を遅延のノブとして

リアルタイムVLMパイプラインにおいて最も影響的な決定は入力解像度です。VLMは画像をパッチに分割し、それぞれのパッチをトークンのシーケンスとしてエンコードします。1280×720のフレームは448×448のクロップよりもはるかに大きなトークンシーケンスを生成し、トランスフォーマーのアテンションがシーケンス長に二乗比例するため、解像度は線形のコストではなく指数的なコストです。ゼロショットオブジェクト検出では、空間精度が重要ですが、ダウンサンプリングは実際のトレードオフです。しかし、シーンレベルの分類タスクでは、「このフレームの主な感情は何か?」と尋ねるのではなく、「すべてのオブジェクトのピクセル座標を教えて」と尋ねる場合、448×448は十分な意味情報を保持しています。

import cv2
import numpy as np
from PIL import Image

def preprocess_frame(frame_bgr: np.ndarray) -> Image.Image:
    # Resize to inference resolution before any VLM processing
    frame_small = cv2.resize(frame_bgr, (448, 448))
    # Convert BGR (OpenCV) to RGB (PIL/transformers)
    return Image.fromarray(cv2.cvtColor(frame_small, cv2.COLOR_BGR2RGB))

フルスクリーンモードを開始 フルスクリーンモードを終了

重要な洞察は、解像度は実際にタスクが要求する情報の粒度に基づいて選択されるべきであり、入力ストリームの解像度に基づいて選択されるべきではないということです。カメラが1080pでキャプチャするけれども、分類タスクが五つの感情状態を区別するだけなら、あなたは使うことのない情報に対して莫大な計算コストを払っていることになります。

パターン2:閉じた語彙での確定的デコーディング

標準的なVLMの使用法では、モデルを無限に続くテキストジェネレーターとして扱います。プロンプトを与えると、確率分布からサンプリングし、自然言語のレスポンスを受け取り、それをパースします。これは前の記事で議論した脆弱性問題の原因であり、また遅延の大きな原因でもあります:高いサンプリングmax_new_tokens は、モデルが生成する各トークンに対して完全な自己回帰ループを実行することを意味します。分類タスクの場合、これを完全に無視できます。モデルに見ているものを説明するように求める代わりに、その出力を有効なラベルの固定された語彙に制限し、それを表現するために必要な最小限のトークン数に生成を制限します。

# A generic set of states tailored to your specific domain
VALID_CLASSES = ['active', 'idle', 'error', 'offline', 'unknown']

def classify_frame(model: torch.nn.Module, processor, image: Image.Image) -> str:
    prompt = (
        f"<image> What is the current operational state shown in this frame? "
        f"You MUST choose ONLY ONE from: {VALID_CLASSES}."
    )

    inputs = processor(
        text=prompt,
        images=image,
        return_tensors="pt"
    ).to(model.device, model.dtype)

    with torch.inference_mode():
        output_ids = model.generate(
            **inputs,
            max_new_tokens=10,   # A single class label needs at most 2-3 tokens
            do_sample=False      # Greedy decoding: deterministic, faster, no temperature needed
        )

    raw_output = processor.decode(
        output_ids[0][inputs.input_ids.shape[-1]:],
        skip_special_tokens=True
    ).strip().lower()

    # Sanitize to alphabetic characters only
    return ''.join(filter(str.isalpha, raw_output))

Enter fullscreen mode Exit fullscreen mode

設定do_sample=Falseはモデルをgreedy decodingに切り替え、各ステップで最も確率の高いトークンを選択します。これによりサンプリングのオーバーヘッドが削除され、出力が完全に確定的になります:同一の入力は常に同一の出力を生成するため、デバッグと次に説明する時系列のスムージングパターンにとって不可欠です。Themax_new_tokens=10 キャップは、ラベルを生成した直後にモデルがほぼすぐに生成を停止することを意味し、誰も要しない説明テキストを続けて生成することではなく。
その結果、あなたは3BパラメータのVLMを高機能で意味に敏感な分類器として、生成モデルではなく使っていることになります。自然言語プロンプティングのゼロショットの柔軟性と、専用の分類ヘッドに近い推論特性を得られます。

時系列平滑法による予測安定性

確定的デコーディングであっても、ライブビデオを処理するVLMはノイズの多い予測を生み出します。照明の変化、動きのぼかし、部分隠蔽、一時的な視覚的アーティファクトが、モデルに連続するフレーム間で一貫性のないラベルを出力させます。生の各フレーム予測を直接下流システムにパイプに接続すると、揺らぎがあり、信頼性の低い出力が得られます。解決策は時系列平滑化です:単一の予測を信頼する代わりに、最近の予測のローリングウィンドウを維持し、多数決を出力します。

from collections import deque, Counter

class TemporalSmoother:
    def __init__(self, window_size: int = 5):
        self.history = deque(maxlen=window_size)

    def update(self, prediction: str) -> str:
        self.history.append(prediction)
        # Return the most common prediction in the window
        return Counter(self.history).most_common(1)[0][0]

フルスクリーンモードを開始 フルスクリーンモードを終了

私たちの推論速度における5フレームのウィンドウは、約4~6秒のタイムコンテキストに相当します。これは一時的なノイズを吸収しながら、本物の状態変化に反応し続けるのに十分です。ウィンドウサイズは主要なチューニングパラメータです:大きなウィンドウは安定性が高いですが反応が遅くなり、小さなウィンドウは反応が速いですがノイズが多いです。大多数の分類タスクでは、3~7フレームが実用的な範囲をカバーします。

パターン4: 分離された共有状態アーキテクチャを使用した非同期推論

前の3つのパターンは推論呼び出し自体を最適化します。このパターンはより根本的なシステム的な問題に対応しています:0.8秒から1.2秒かかるVLM推論呼び出しは、実行しているスレッドをその間完全にブロックします。ビデオキャプチャと推論が同じスレッドで実行されている場合、ストリームは推論の速度でカクつく代わりに、カメラの速度でカクつきます。

素朴な解決策は、Pythonの標準ライブラリのqueue.Queueを使用してスレッド間でフレームを渡すことです。しかし、これによりコンシューマー間の競合が生じます:レンダリングスレッドとAIスレッドが同じキューから読み取ると、フレームを消費し合い、お互いのデータを奪い合って、深刻な視覚的なステータスの停止と推論サイクルのスキップを引き起こします。本格的な解決策は非同期共有状態パターンです。 で粒度化されたロックが行われます。ビデオキャプチャスレッドはプロデューサーとして機能し、継続的に共有される「最新フレーム」ポインタを上書きします。レンダリングスレッド(メインスレッド上で実行され、macOSおよびWaylandでのOpenCV UI操作に必須)とAIバックグラウンドスレッドは独立したコンシューマーとして動作し、次のサイクルに備え準備ができたときに最新フレームをローカルメモリにコピーします.

import threading
import time
import numpy as np
import cv2
import torch
from typing import Optional, Any

class SharedState:
    """
    Thread-safe state container.
    The lock is strictly granular: it is only held for memory assignment/copying,
    never during expensive I/O or AI inference operations.
    """
    def __init__(self):
        self.latest_frame: Optional[np.ndarray] = None
        self.prediction: str = "WAITING"
        self.lock = threading.Lock()
        self.running: bool = True

shared = SharedState()

def video_capture_worker(source: int = 0) -> None:
    """Reads frames at hardware speed and updates the shared state."""
    cap = cv2.VideoCapture(source)

    while shared.running:
        ret, frame = cap.read()
        if not ret:
            time.sleep(0.01)
            continue

        with shared.lock:
            # Overwrite with the freshest data.
            # Pointer assignment is fast enough to barely hold the lock.
            shared.latest_frame = frame

    cap.release()

def inference_worker(model: torch.nn.Module, processor: Any) -> None:
    """Consumes the latest frame at the AI's maximum throughput rate."""
    smoother = TemporalSmoother(window_size=5)

    while shared.running:
        with shared.lock:
            # Deep copy to prevent OpenCV from mutating the array during inference
            frame = shared.latest_frame.copy() if shared.latest_frame is not None else None

        if frame is None:
            time.sleep(0.05)
            continue

        try:
            image = preprocess_frame(frame)
            raw_pred = classify_frame(model, processor, image)
            smoothed_pred = smoother.update(raw_pred)

            with shared.lock:
                shared.prediction = smoothed_pred

        except torch.cuda.OutOfMemoryError:
            # Handle temporary VRAM spikes gracefully without killing the thread
            with shared.lock:
                shared.prediction = "OOM_ERROR"
            time.sleep(1.0)

        except Exception as e:
            # Catch corrupt frames or tensor mismatches
            with shared.lock:
                shared.prediction = "ERROR"

# Initialize background workers as Daemon threads
threads = [
    threading.Thread(target=video_capture_worker, args=(0,), daemon=True),
    threading.Thread(target=inference_worker, args=(model, processor), daemon=True),
]

for t in threads:
    t.start()

# Main Thread UI Loop
# UI libraries (cv2.imshow) must run on the main thread to prevent OS-level crashes.
while shared.running:
    with shared.lock:
        frame = shared.latest_frame.copy() if shared.latest_frame is not None else None
        label = shared.prediction

    if frame is not None:
        cv2.putText(frame, label.upper(), (30, 50),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
        cv2.imshow("Live Classification", frame)

    # 30 FPS rendering limit (33ms) + graceful shutdown
    if cv2.waitKey(33) & 0xFF == ord('q'):
        shared.running = False

cv2.destroyAllWindows()

Enter fullscreen mode フルスクリーンモードを終了する

ここでの重要な設計原則は粒度の細かいロックです:ロックが取得され、NumPy配列がメモリ内にコピーされる(これはマイクロ秒単位でかかる)、そしてロックがすぐに解放されます。1秒のVLM推論呼び出しの間にロックを保持すると、すべての3つのコンポーネントがシリアライズされ、そのアーキテクチャの目的を達しないことになります。この構造では、あなたのビデオキャプチャスレッドはハードウェアのフレームレート(例えば30fps)で動作し、レンダリングループは30fpsでフレームを表示し、推論スレッドは自分自身の非同期レート(1fps)で動作します。3つのシステムは時系列上で独立しており、それぞれのハードウェアの制限のみによって制限されます。

ベンチマークの概要

NVIDIA RTX A4500(20GB GDDR6、Ampereアーキテクチャ)でPaliGemmaを使用し、bfloat16で三つのストリームのライブビデオシナリオで完全なパイプラインを実行すると、非常に安定したパフォーマンスプロファイルが得られます。入力解像度を448 × 448に制限し、greedyデコーディング戦略を通じて出力を最大10個の新しいトークンに制限すると(do_sample=False), システムはフレームごとに0.8秒から1.2秒の推論遅延を達成します。5フレームの時系列平滑化ウィンドウと組み合わせると、この構成は信頼性のある状態分類を保証し、分離されたアーキテクチャによりビデオキャプチャスレッドは推論のボトルネックに依存せず安定した25fpsを維持できます。

比較として、LLaVA-v1.6-Mistral-7BがNVIDIA L4でオープンボキャブラリーゼロショット検出を実行する際、1フレームあたり8.13秒かかります。ハードウェアが直接等価ではありませんが、そのギャップの大きさは、アーキテクチャの制約が、生の計算能力ではなく、差の大部分を説明していることを確認します.

このアーキテクチャが意味を成る時

このパターンは、タスクが固定されたラベルセットに分類可能で、ライブストリームの連続処理が必要であり、静的な画像のバッチ分析ではなく、データプライバシー要件が外部APIにフレームを送信することを排除し、秒単位の遅延を100ms単位の遅延よりも許容できる場合に適しています。コンベアベルト速度での真のリアルタイムレスポンスが必要な場合、50ms単位の遅延は交渉不可能な場合、これは正しいツールではありません。その場合、あなたはYOLOの領域に戻り、前の記事で説明したようなパイプラインを使用します:VLMを利用してデータセットを一夜で自動アノテーションし、その後、生産デプロイ用の専用の軽量分類器をトレーニングします。

結論

「VLMsは動画処理には遅すぎる」という主張と「VLMsは生産用動画パイプラインで動作する」という主張の間のギャップは、主にハードウェアの問題ではありません。それはアーキテクチャの問題です。PaliGemmaのようなコンパクトなモデルを選択すること、タスクが実際に必要とする解像度に制限すること、閉じた語彙での確定されたデコーディングを強制すること、予測を時間的に滑らかにすること、推論をキャプチャとレンダリングから分離すること:これらはどれもより大きなGPUを必要としません。それらは、モデルに実際に何を求めているかを慎重に考えること、そしてその制約を逆にするのではなく、それに基づいてパイプラインを構築することを必要とします。

モデルのロードからマルチスレッド推論までの完全なパターンは、150行以下のPythonで収まる。ライブビデオストリーム上のゼロショットセマンティック分類にとっては、それが合理的な価格だ。