GENZITSU / UsefulMaterials

34 stars 0 forks source link

almost weekly useful materials - 02/07 - #145

Open GENZITSU opened 3 months ago

GENZITSU commented 3 months ago

日本語CLIP 学習済みモデルと評価用データセットの公開

LIONデータセットから日本語のみを抜き出したデータセットを用いて日本語に特化したCLIP作成の試みがまとめられている記事。

大規模データセットを用いた大規模モデルを学習する際の種々ノウハウが綴られている

既存モデルについて

rinna/japanese-clip-vit-b-16 CC12M を和訳したもので学習が行われており、訓練データは1200万件と少ない

stabilityai/japanese-stable-clip-vit-l-16 もCC12MとSTAIR Captionsで学習されたものであり、学習サンプル数は比較的少ないため十分に訓練されていない可能性があります。

laion/CLIP-ViT-B-32-xlm-roberta-base-laion5B-s13B-b90k laion/CLIP-ViT-H-14-frozen-xlm-roberta-large-laion5B-s13B-b90k LAION-5B で学習されているため訓練データは50億件ですが、多言語で学習をされており、日本語に特化させたモデルを作成すれば日本語が関わるタスクにおいてはこれを上回る性能を達成できる可能性がある

学習周りの設定

OpenCLIPの多言語版 と イタリア語CLIP の事例を参考に実施

full scratch学習ではなく、画像エンコーダは laion/CLIP-ViT-B32-laion2B-s34B-b79k を、テキストエンコーダは rinna/japanese-roberta-base を使うように実装。

ただし、両エンコーダの出力の次元数は異なるため、テキストエンコーダの最後に線形層を追加。

この状態で最初からモデル全体を訓練すると、勾配が不安定になり学習済みモデル部分の重みが損なわれてしまうおそれがあるため、最初しばらくはテキストエンコーダの最後の線形層のみを一定の学習率で訓練しました。これはLP-FT(linear probing then fine-tuning)と呼ばれるテクニックです

バッチサイズはCUDA OOM(out of memory)が発生しない限界を探索し、それを固定しました。今回はNVIDIA A100 40GBのGPUを8枚使用し、混合精度で計算したところ、グローバルなバッチサイズが4352となりました。

次に、予算制約と1ステップあたりの時間から、訓練ステップ数を44万としました。最後に、学習が安定するようなAdamの基本学習率を探索し、2e-5としました。学習率のスケジューリングには、線形ウォームアップとコサイン減衰を組み合わせて用いました。

学習結果

ロスが上がっているのに、下流タスクの性能は上がっているという現象に遭遇

スクリーンショット 2024-01-31 14 53 05

また最終的な評価指標は以下のようになった。

スクリーンショット 2024-01-31 14 54 58 スクリーンショット 2024-01-31 14 55 05

意外なことに日本語に特化した学習を行なったものよりも、多言語データで学習したモデルの方が性能が高いという結果になっている。

また、ImagenetやFood101などの海外ドメイン由来のデータで日本語性能を評価することには注意が必要そうということがわかる。

実装に役立ったレポジトリ

Distributed Data Parallel (DDP)

DPとDDPの違いは複数GPUで学習するときのDP(Data Parallel)とDDP(Distributed Data Parallel)の違いが詳しい。

ざっくりうと、DDPはそれぞれのGPUごとにデータ読み込み/forward/backwardが独立して行われるが、DPはあるGPUが起点となってデータ読み込みやloss計算を管理・実行しているため、使えるならDDPを使った方が良い

サンプル学習コードは以下


import os
import torch
import torch.distributed as dist
import torch.distributed.nn
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP

def main():
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "12355"
    n_gpu = torch.cuda.device_count()
    mp.spawn(main_worker, nprocs=n_gpu, args=(n_gpu,))

def main_worker(rank, n_gpu):
    dist.init_process_group("nccl", rank=rank, world_size=n_gpu)
    model, preprocess, tokenizer = build_models(device="cpu")
    torch.cuda.set_device(rank)
    model.cuda(rank)
    model = DDP(model, device_ids=[rank])
    criterion = ClipLoss(cache_labels=True, rank=rank)

    for epoch in range(CFG.num_epochs):
        train_data.set_epoch(epoch)
        train_loss, current_steps = train_single_epoch(
            gpu=rank,
            model=model,
            criterion=criterion,
            optimizer=optimizer,
            dataloader=train_data.dataloader,
        )

ただし注意点もあり、

まず、勾配を計算しモデルの重みを更新するtrain_single_epoch関数内で、モデルの損失を計算する際には、必ずモデルのcall関数またはforward関数を経由するようにしてください。すなわち、以下のようにmodel(pixel_values=X_i, input_ids=X_t)を経由して損失を計算するということです。

初期の実装段階において、私たちはimage_featuresとtext_featuresを計算するためにそれぞれget_image_features関数とget_text_features関数を用意して使っていたので、実際にこの状態に陥っていました。しかし、このバグがあっても訓練中の損失は下がっていきました(正しく実装した場合よりも下がり方が遅くなるはずです)。

もう一つの注意点は、対照損失の実装です。ナイーブな実装では比較対象の数がローカルに存在する128個だけになってしまいます。比較対象が少ないと、モデルに解かせるタスクが簡単になるため、結果として収束後の性能が低下する可能性があります。この問題を回避するためには、対照損失の計算時にall_gather関数を呼び出すことで、GPU間でimage_featuresとtext_featuresを共有する必要があります。

ただし、このとき、前述のmain_worker関数内で、GPU間通信のバックエンドとして NCCL を指定する必要があります。これは、all_gather関数をGPUでサポートしているのがNCCLのみだからです。詳しくは 各バックエンドの対応表 を見てください。

def gather_features(image_features, text_features):
    all_image_features = torch.cat(
        torch.distributed.nn.all_gather(image_features), dim=0
    )
    all_text_features = torch.cat(torch.distributed.nn.all_gather(text_features), dim=0)
    return all_image_features, all_text_features

シーケンシャルアクセスによるデータローダーの最適化

大規模データセットでランダムアクセスをやろうとすると、ランダムアクセス自体のコストが嵩んでデータのGPUの計算に待ち時間が生まれることがある。

これを緩和するためのテクニックがシーケンシャルアクセスで

近年の大規模学習では、ディスクの先頭から順にデータにアクセスしていくシーケンシャルアクセス(sequential access)を採用することがあります。この方法により、データローダーの効率が向上し、訓練の高速化が期待されます。なお、シーケンシャルアクセスで擬似的にランダムネスを実現するためには、現在位置から一定の数のデータをバッファに詰めておき、そこからシャッフルした上でバッチを取り出します。

これをサポートしているライブラリがpytorchであればWebDataset, tensorflowであればTFRecord。

WebDatasetはその名の通り、web上のデータやクラウドストレージのデータも参照できるためさらに相性が良い。

ただしこれを正しく用いるためには、以下に注意する必要有り

確認はかなり大変そう。

コメント

本業でこういう規模のモデルを学習することはほぼないため、知らないことばかりだった。 ちなみにLoRAはうまくいかなかったとのこと。事前学習にはやっぱり向かないんだろうか。

出典

GENZITSU commented 3 months ago

日本ディープラーニング協会主催 NeurIPS 2023 技術報告会講演資料

TDAI LabがまとめたNeruIPSから見た近年のAIトレンドまとめスライド

とても良いまとめで参考になることが多いが、特に気になったところのみ抜粋

LLMのアライメント手法の簡素化が進んでいる

スクリーンショット 2024-02-06 22 07 28

LLMになってもデータは重要

スクリーンショット 2024-02-06 22 07 49

予算制約がある中で、どのデータを選択して学習に用いるかという話題

スクリーンショット 2024-02-06 22 10 38

生成AIで作成したデータを用いた学習    スクリーンショット 2024-02-06 22 10 51

少ないデータでも何epochかは回した方が良い (通常は1epochだったらしい...

スクリーンショット 2024-02-06 22 11 05

マルチモーダルLLMの基本着想

スクリーンショット 2024-02-06 22 13 29

スクリーンショット 2024-02-06 22 13 44

マルチモーダルでもCoTは有効に働きうる

スクリーンショット 2024-02-06 22 14 48

コメント

トレンド把握するのにちょうど良いまとめだった。 生成AIで作成したデータによる学習の論文は詳しく見てみたい

出典

GENZITSU commented 3 months ago

2024年のPythonプログラミング

uzabaseの2024年版Pythonベストプラクティス集的なやつ

ためになったところだけメモ (自分の理解に収まるもののみ)

複数のプログラミング言語のバージョンをまとめて管理できるツールが存在する

また、Pythonを含む複数のプログラミング言語のバージョンをまとめて管理できるツールもあります。anyenvasdfが有名ですが、個人的なおすすめはmiseです(発音は "meez")。以前は「rtx」という名前でしたが、NVIDIAのGPUシリーズ「RTX」との混乱を避けるため「mise」に変更されました。

Python 3.10以降は Union[str, int] や Optional[int] をそれぞれ str | int や int | None と短く書けるようになりました。from typing import Union, Optional などのimport文の記述も不要になります。 また、3.10以降は型の別名を定義するときに TypeAlias を使って明示的に定義できるようになりました。

from typing import TypeAlias

ItemPrices: TypeAlias = list[tuple[str, int]]

dataclassにおけるfrozen=Trueおよびdataclasses.replaceによる別インスタンス生成 ちなみにfrozen=TrueはPydanticのdataclassでも利用可能

import dataclasses
from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    name: str
    deactivated: bool = False

user1 = User("taro")  # User(name="taro")でもOK
user2 = User("hanako", True)
user3 = dataclasses.replace(user2, deactivated=False)

煩わしいやつが解消されている

さらに、Python 3.12 からはf"name={user["name"]}"のようにf文字列全体の引用符と同じ引用符を式の中でも使用できるようになりました。以前はf"name={user['name']}"のように異なる引用符を使う必要がありましたが、そのような配慮は不要になりました。

click, Typerを用いたより可読性の高いCLIアプリケーション構築

import click

@click.command()
@click.argument("name")
@click.option("--greeting", default="こんにちは", help="挨拶の言葉(デフォルト: こんにちは)")
def main(name, greeting):
    print(f"{greeting}, {name}!")

if __name__ == "__main__":
    main()
import typer

app = typer.Typer()

@app.command()
def main(name: str, greeting: str = "こんにちは"):
    print(f"{greeting}, {name}!")

if __name__ == "__main__":
    app()

サブコマンドを作成することも可能

import typer

app = typer.Typer()

@app.command()
def hello(name: str):
    print(f"こんにちは, {name}!")

@app.command()
def good_morning(name: str):
    print(f"おはよう, {name}!")

if __name__ == "__main__":
    app()

コメント

個人的にはdataclassのfrozen=Trueとclick/typerが一番衝撃的だった。 Python3.12からのf文字のやつは、慣れの問題で結局取り扱いしづらいかも...

出典

GENZITSU commented 3 months ago

AzureでRAGをガンガン試行錯誤してみて得たナレッジを紹介します!

KDDIで行われたRAGによるデバイスサポートチャットの試行錯誤が綴られているスライド

勉強になったところだけ抜粋

スクリーンショット 2024-02-05 23 35 49

スクリーンショット 2024-02-05 23 37 02

スクリーンショット 2024-02-05 23 37 37

コメント

普通のことではあるが、index登録時に入れるテキストを整形すること、特定性の高い情報はmetadataに入れておくとよいという感じか

出典

GENZITSU commented 3 months ago

生産性向上のために身に着けたい10のこと

世界一流エンジニアの思考法という本のまとめ的な内容を記した記事

個人的に勉強になった事項だけ抜粋

コメント

とくに10番が刺さった。時間がある身分なのでつい1日に設定した成果をベースにやってしまいがちだったが、これだと生産性向上という観点では成長しづらいなというのは確かに納得。。

出典