マルチモーダルLLM（Molmo）の仕組み ‐ 世界モデル（0）



# 本章について
本連載は複数に分かれていますが、本章はメインではなくあくまでサブの記事です。章で言えば「第0章」に当たるような内容となっています。「マルチモーダルLLM（Molmo）の仕組み ‐ 世界モデル （1）」以降の内容がメインとなります。

本章では、読み始める前に事前の知識としてこれは知っておいた方が良いというものや、本連載を書く中で見つけた便利なものをいくつかご紹介します。体系的に解説するわけではなく、雑多な知識やツールを羅列しているだけなので、知らない用語や興味のある項目だけをざっと眺めていただければと思います。

[ 次の記事 ]

https://qiita.com/shinya_2024/items/cf8e89771e682b93a23f


# 論文とモデル

## Transformer

Transformer は Vaswani et al. (2017) によって提案されたアーキテクチャで、従来の再帰的な構造を廃し、Self-Attention 機構を軸にしています。Self-Attention は入力系列内のすべての位置を相互に参照しながら特徴を集約し、Multi-Head Attention を通じて異なる視点で情報を捉え、その結果を統合します。さらに、各サブレイヤーには Residual Connection と LayerNorm が組み合わされ、勾配の消失や爆発を抑えながら安定して学習を進めることができます。
* 主要な構成要素
    - __Self-Attention__
    入力系列のすべての位置同士で情報をやり取りし、各トークンの重み付けされた表現を生成する機構
    - __Multi-Head Attention__
    複数の “Attention Head” を並列化し、異なる視点で情報を集約した後、統合（MergeHeads）する仕組み
    - __Position Encoding__
    トークンの位置情報を埋め込みに付与し、系列順序を認識させるために用いられる
    - __Feed-Forward Network (FFN)__
    各トークンごとに適用される 2層の全結合ネットワークで、非線形変換を担う
    - __Residual Connection & LayerNorm__
    各サブレイヤーの出力に残差接続を挟み、その後 Layer Normalization を適用することで、勾配の安定化と高速収束を促進する

https://arxiv.org/abs/1706.03762

## OpenAI CLIP

OpenAI が開発した CLIP（Contrastive Language–Image Pretraining）は、画像とテキストの対比学習を用いることで、自然文と画像を同一の埋め込み空間にマッピングするモデルです。大量の画像テキストペアを用いて学習し、画像→テキスト、テキスト→画像のタスクを同じアーキテクチャでこなせる点が特徴です。主な構造要素は以下のとおりです。

- __ビジョンエンコーダ__
通常、ResNet や Vision Transformer（ViT）を用いて画像を固定長の埋め込みベクトルに変換する。
- __テキストエンコーダ__
通常のテキストトークナイザ＋Transformer でテキストを埋め込みベクトルに変換する。
- __コントラスト学習__
画像とそれに対応するテキストをペアとし、他のすべてのペアをネガティブサンプルとして利用することで、対応する画像／テキストペアは相互に類似度を高く、その他とは低くなるように、埋め込み空間を構築する。
CLIP は「Zero-shot」設定でも高い精度を発揮し、数多くのマルチモーダルタスクでベースモデルとして活用されています。

https://arxiv.org/abs/2103.00020

### Vision Transformer (ViT)

ViT は Dosovitskiy et al. (2020) が提案したモデルで、画像を固定サイズのパッチ（たとえば16×16ピクセル）に分割し、それぞれのパッチをトークンとして Transformer に入力します。各パッチには位置情報を付与し、Transformer Encoder を通じてパッチ間の相互作用を学習します。最終的に「__CLS トークン__」として配置された特別な埋め込みが、画像全体の特徴を要約します。

* 画像をパッチに切り出した後、線形層によって __d\_model__ 次元の埋め込みに変換します
* Transformer を通じて得られる CLS トークンの出力ベクトルが、分類やキャプショニングなどのタスクに利用されます
* 大規模データセットで事前学習すると、CNN ベースのモデルと同等以上の性能を示しながら、並列処理の利点を活かせます

https://arxiv.org/abs/2010.11929


# 基礎用語

## CLS トークン（クラストークン）

CLS トークンは、Transformer 系モデルで系列の先頭に追加される特別なトークンで、最終的に「系列全体をざっくりまとめた」ベクトルとして振る舞います。直感的には、文章の要点をひとつのメモにまとめるようなイメージで、それぞれのトークン（あるいは画像パッチ）が持つ情報を自己注意によって吸い上げ、CLS が「総合的な要約」として出力されます。画像処理の ViT では、パッチごとに計算された特徴を CLS が受け取って融合し、「画像全体の特徴」を表現します。最終層で得られる CLS のベクトルは下流タスク（たとえば画像分類やキャプション生成）の入力として非常に重要な役割を担います。

## d\_model

d\_model は Transformer や派生モデル全体で「各トークンが持つ埋め込みベクトルの次元数」を指します。たとえば d\_model=768 と設定すると、入力テキストは (batch\_size, seq\_len, 768) のテンソルに変換されます。自己注意の計算では、クエリ (Q)、キー (K)、値 (V) がすべて d\_model 次元に基づくマッピングを受け、複数のヘッドに分割されて並列処理されます。各ヘッドは d\_model/num\_heads 次元のベクトルを扱い、最終的に統合 (MergeHeads) されて再び d\_model 次元に戻されます。こうした仕組みを通じて、テキストや画像といった異なる入力が同じ次元空間で扱われ、モデル全体で整合的に情報をやり取りできます。

## Vocabulary Size

Vocabulary Size はモデルが許容するユニークなトークン数の総計を指し、埋め込み層の行数や最終出力のロジット次元として使われます。例えば GPT-2 の場合、約 50,257 個のトークンを語彙としており、出力は (batch\_size, seq\_len, 50257) のロジットになります。最終線形層ではこの語彙サイズ分だけ非正規化スコアを出力し、ソフトマックスを適用して「次に予測されるトークンの確率分布」を得ます。


# モデル調査時に役立った “テクニック”

## VSCode
VSCode にはコードの折りたたみと展開を素早く行うショートカットが用意されています。これらを使いこなすと、特に大規模なコードベースにおいて必要な部分だけを素早く表示し、不要な部分を隠すことができます。

```terminal
# Windows/Linux
コード全体を折りたたむ: Ctrl+K, Ctrl+0
コード全体を展開する: Ctrl+K, Ctrl+J
指定範囲を折りたたむ（カーソルがあるブロックだけ）: Ctrl+Shift+\[
指定範囲を展開する: Ctrl+Shift+]
折りたたみレベルを指定して再帰的に折りたたむ（例: レベル2）: Ctrl+K, Ctrl+2

# macOS
コード全体を折りたたむ: ⌘+K, ⌘+0
コード全体を展開する: ⌘+K, ⌘+J
指定範囲を折りたたむ（カーソルがあるブロックだけ）: ⌥+⌘+\[
指定範囲を展開する: ⌥+⌘+]
折りたたみレベルを指定して再帰的に折りたたむ（例: レベル2）: ⌘+K, ⌘+2
```

## Python

### データサイズの調べ方
こちらに関しては "役に立った" というより "気になった" トピックです。

モデル内部で処理されるデータを調べるために、pickleでオブジェクトを保存したりしていたのですが、データサイズが異様に大きいことがありました。500 MB以上です。当該インスタンスは様々なデータを含む辞書データであったため、ライブラリを使って再帰的にインスタンス内部のサイズを調べてみたところ、1KB も無いような小さな値が返ってきました。この様になった理由は単純で以下のようなメソッドを使っていたからです。

```python
import sys
sys.getsizeof()
```
```python
from pympler import asizeof
asizeof.asizeof(obj)
```

上記のライブラリで表示されるインスタンスのサイズはCPU上のサイズのみで、GPU上のオブジェクトサイズは反映されません。GPU上に展開されたものに対し上記方法で調べようとした結果、単に参照先のポインタの容量などが返ってきてしまい、実際のpickleのファイルサイズと比較すると著しく小さな容量が返されていたのです。
この問題を解決するには、GPU テンソルのデータサイズを手動で計算する必要があります。PyTorch テンソル `obj` の場合、次の式でバイト数を求めるとおおよその占有メモリ量が得られます。

```python
obj.numel() * obj.element_size()
```

この方法を使えば、GPU 上のテンソル部分も含めて「純粋にオブジェクトが占有するメモリ量」に近い数値が得られます。しかしこれでも、メモリのサイズとpickleのファイルサイズは一致しません。何故なら、上記はあくまで Tensor の生データサイズであり、実際に GPU 上で確保されているメモリはこれより大きくなるからです。
より専門的な話として、PyTorch の内部では、以下のような要因で「実際に確保されるメモリ量」が増減します。

- アロケーション（割り当て）単位とアライメント
GPU は 256 バイト単位など一定のブロックサイズでメモリを管理するため、たとえば 100 MB のテンソルでも “128 MB” などに丸めて確保されることがあります 。
- メモリプール（キャッシュ）
PyTorch は一度確保したメモリを「キャッシュ」として保持し、後続のアロケーションで再利用するため、 `torch.cuda.memory_reserved()` に示される値は「キャッシュも含む予約済み合計メモリ」になります｡`memory_allocated()` とは異なる数値を報告しがちです。
- CUDA コンテキストとドライバオーバーヘッド
CUDA のコンテキストやスレッドバッファなど、PyTorch 以外が確保するメモリも含まれるため、`nvidia-smi` に表示される「Used」量はさらに大きくなります 。

これらを踏まえたうえで、「実際に GPU 上でどれだけメモリが確保されているか」を段階的に確認するには、次のようなコードを順に実行するとよいでしょう。
```python
import torch
print("Allocated:", torch.cuda.memory_allocated()/1024**2, "MB")
print("Reserved :", torch.cuda.memory_reserved()/1024**2, "MB")
print("Peak     :", torch.cuda.max_memory_allocated()/1024**2, "MB")
```
```bash:terminal
nvidia-smi
```
最終的に、用途に応じてこれらの指標を組み合わせることで「おおよその GPU メモリ使用状況」をつかめます。

私はメモリなどに関しては特に造詣が深くないので、ここまで色々と調べ回っていましたが、結局要因を確定出来るほどの理解にも至らず、思っていたほど単純ではありませんでした。いずれまた、メモリとは本格的に向き合わないといけないような気がするので、その際に格闘しようかと思います（笑）。











### Python 関数の再帰

「再帰関数」というワードはどこかでちらっと見たことはありましたが、お恥ずかしい話、なんなのかよく知りませんでした。今回コードを調べるために、ChatGPT に様々なコードを生成してもらっていると再帰関数に遭遇し、「えっ、これOKなのか！？」と今更ながら驚きました。さすがは ChatGPT。
とはいっても再帰関数は理屈を知ったところで自分で使いこなすのは難しそうです。メソッドの中でメソッド自体を再帰的に使用していいというだけですが、頭の中をよく整理して使わないときれいに動いてくれないので、少々実装に時間がかかると思います。しかし、これこそまさにAIの出番だなと思いました。理屈は難しくないが、少々面倒なことにはどんどんAIを使っていきましょう。

```python
def compare_shape(obj1, obj2, path=""):
    """
    obj1 と obj2 の「形状」や「構造」を再帰的に比較する関数です。
    ・Tensorの場合は、obj.shape を比較します。
    ・辞書の場合は、すべてのキーが存在するか、かつ各要素の形状が一致しているか再帰的に比較します。
    ・リスト/タプルの場合は、要素数と各要素の構造を比較します。
    ・それ以外の場合は、型が一致しているかを比較します。
    差分がある場合は、そのパスと内容を表示し、最終的に True を返します。差分がなければ False を返します。
    """
    differences = False
    msg=""
    # Tensorの場合: shape のみ比較
    if isinstance(obj1, torch.Tensor) and isinstance(obj2, torch.Tensor):
        if obj1.shape != obj2.shape:
            msg=f"Difference at {path}:  {obj1.shape} vs {obj2.shape}"
            differences = True
    # 辞書の場合: キーの存在と各値の形状を再帰的に比較
    elif isinstance(obj1, dict) and isinstance(obj2, dict):
        keys1 = set(obj1.keys())
        keys2 = set(obj2.keys())
        all_keys = keys1.union(keys2)
        for key in all_keys:
            new_path = f"{path}.{key}" if path else str(key)
            if key not in obj1:
                msg=f"Difference at {new_path}: key missing in first object"
                differences = True
            elif key not in obj2:
                msg=f"Difference at {new_path}: key missing in second object"
                differences = True
            else:
                if compare_shape(obj1[key], obj2[key], new_path):
                    differences = True
    # リストまたはタプルの場合: 要素数と各要素の構造を比較
    elif isinstance(obj1, (list, tuple)) and isinstance(obj2, (list, tuple)):
        if len(obj1) != len(obj2):
            msg=f"Difference at {path}: Length mismatch: {len(obj1)} vs {len(obj2)}"
            differences = True
        for i, (item1, item2) in enumerate(zip(obj1, obj2)):
            new_path = f"{path}[{i}]"
            if compare_shape(item1, item2, new_path):
                differences = True
    # 上記以外の場合: 型情報のみで比較
    else:
        if type(obj1) != type(obj2):
            msg=f"Difference at {path}: {type(obj1).__name__} vs {type(obj2).__name__}"
            differences = True
    return differences, msg
```


### 標準出力が長すぎる場合の対応

LLM のようにループが多い状況では、ブレークポイントで止まりすぎてデバッグするのが厄介です。また `print()` デバッグをしようにも、出力が大量になりすぎるため標準出力じゃ表示しきれなかったり、そもそも Colab の UI が見づらかったりします。そこで以下のように標準出力をファイルにリダイレクトする方法が便利でとてもよい解決策になります。結果を記録出来る上、`print` 文だけで済むので小難しい話も一切ありません。後からファイルを検索して必要な箇所だけを抜き出せるため、大量のログが出るようなループ内デバッグに本当に重宝します。

```python
import sys
# sys.stdoutを追記モードのログファイルに切り替え（appendモードなので毎回出力が追加される）
original_stdout = sys.stdout
sys.stdout = open(f"/root/.cache/huggingface/modules/transformers_modules/cyan2k/molmo-7B-D-bnb-4bit/filename", "a", encoding="utf-8")

# …
# 略
# …

sys.stdout = original_stdout
```

### GPU

#### メモリを開放して次の処理に備える

モデルや大きなテンソルを GPU から解放し、不要なキャッシュをクリアすることで、GPU メモリ不足を防ぎます。手順は以下のとおりです。

```python
# モデルオブジェクトを削除
del model  

# Python のガベージコレクションで不要オブジェクトを解放
import gc
gc.collect()  

# PyTorch の GPU キャッシュを空にする
import torch
torch.cuda.empty_cache()
```

この手順を挟むことで、モデルを編集後に連続でロードしたりしても、前の残骸を残さず GPU メモリを確保した状態で次の処理に移れます。





# おわりに
以上が第0章の内容です。本章はあくまで、知らないものや興味のある部分の羅列なのでざっとチェックしていただき、以降の各章で必要な場面に戻ってきて参照していただければと思います。