almost weekly useful materials - 02/07 -
Opened this issue · 5 comments
日本語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としました。学習率のスケジューリングには、線形ウォームアップとコサイン減衰を組み合わせて用いました。
学習結果
ロスが上がっているのに、下流タスクの性能は上がっているという現象に遭遇
また最終的な評価指標は以下のようになった。
意外なことに日本語に特化した学習を行なったものよりも、多言語データで学習したモデルの方が性能が高いという結果になっている。
また、ImagenetやFood101などの海外ドメイン由来のデータで日本語性能を評価することには注意が必要そうということがわかる。
実装に役立ったレポジトリ
- mlfoundations/open_clip
- rinnakk/japanese-clip
- 特に、再現実装の mlfoundations/open_clip は、 公式レポジトリ では非公開の訓練コードを含んでいる貴重な実装です。 また、ソースコードと合わせて公開している学習済みモデルは公式モデルよりも高い精度を達成しており、推論のみでの利用であっても非常に有用です。
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上のデータやクラウドストレージのデータも参照できるためさらに相性が良い。
ただしこれを正しく用いるためには、以下に注意する必要有り
- シャード内のデータをアクセスする際にちゃんとシャッフルされること
- GPUごとに用いる乱数シードがことなること
- 各GPUでシャードを読む順番が異なっていること
- ローカルなバッチの中身がシャッフルされていること
- 各エポックでシャードを読む順番が異なっていること
確認はかなり大変そう。
コメント
本業でこういう規模のモデルを学習することはほぼないため、知らないことばかりだった。
ちなみにLoRAはうまくいかなかったとのこと。事前学習にはやっぱり向かないんだろうか。
出典
日本ディープラーニング協会主催 NeurIPS 2023 技術報告会講演資料
TDAI LabがまとめたNeruIPSから見た近年のAIトレンドまとめスライド
とても良いまとめで参考になることが多いが、特に気になったところのみ抜粋
LLMのアライメント手法の簡素化が進んでいる
LLMになってもデータは重要
予算制約がある中で、どのデータを選択して学習に用いるかという話題
少ないデータでも何epochかは回した方が良い (通常は1epochだったらしい...
マルチモーダルLLMの基本着想
マルチモーダルでもCoTは有効に働きうる
コメント
トレンド把握するのにちょうど良いまとめだった。
生成AIで作成したデータによる学習の論文は詳しく見てみたい
出典
2024年のPythonプログラミング
uzabaseの2024年版Pythonベストプラクティス集的なやつ
ためになったところだけメモ (自分の理解に収まるもののみ)
複数のプログラミング言語のバージョンをまとめて管理できるツールが存在する
また、Pythonを含む複数のプログラミング言語のバージョンをまとめて管理できるツールもあります。anyenvやasdfが有名ですが、個人的なおすすめは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文字のやつは、慣れの問題で結局取り扱いしづらいかも...
出典
AzureでRAGをガンガン試行錯誤してみて得たナレッジを紹介します!
KDDIで行われたRAGによるデバイスサポートチャットの試行錯誤が綴られているスライド
勉強になったところだけ抜粋
コメント
普通のことではあるが、index登録時に入れるテキストを整形すること、特定性の高い情報はmetadataに入れておくとよいという感じか
出典
生産性向上のために身に着けたい10のこと
世界一流エンジニアの思考法という本のまとめ的な内容を記した記事
個人的に勉強になった事項だけ抜粋
-
- 理解に時間をかける
- タスクや物事の本質を理解してから仕事に取り掛かる
-
- 「Be Lazy」というマインドセット
- より少ない時間で最大の効果を生む
- 特に優先順位をつけて、優先順位に従って1つ1つにフォーカスすること
-
- Fail Fastの精神をもつ
- まずはやってみて早めにFBを得ることを優先する
-
- 「何もググらず即実装できる」レベルを増やす
- ググらず実装できるようになると早い
-
- マルチタスクをやらない
- ワシントン大学の研究によると、生産性が40%低下、仕事にかかる時間が50%増加、ミスの発生が50%増加するそう
-
- 知らないことを恥じない
- ディスカッションの目的を「お互いが持っている意見を交換して、知識や考えを深めること」で、的外れかどうかは関係なく、参加しているメンバーの意見を交換し、全員が理解を深めることが重要
-
- 成果ではなく時間で仕事の区切りをつける
コメント
とくに10番が刺さった。時間がある身分なのでつい1日に設定した成果をベースにやってしまいがちだったが、これだと生産性向上という観点では成長しづらいなというのは確かに納得。。