学習トランスフォーマーコード入門:パート1 – セットアップ

'学習トランスフォーマーコード入門:パート1 - セットアップ' can be condensed to '学習トランスフォーマーコード入門:パート1'

nanoGPTを出発点としたTransformerの4部構成の探求

Josh Riemerによる写真、Unsplash

あなたについてはわかりませんが、私は論文を読むよりもコードを見る方が簡単です。AdventureGPTを開発しているとき、私は600行程度のPythonで実装されたReAct論文の実装であるBabyAGIのソースコードを読むことから始めました。

最近、優れたCognitive Revolution Podcastのエピソード33でTinyStoriesという最近の論文を知りました。TinyStoriesは、数百万(数十億ではない)のパラメータでトレーニングされたモデルが高品質のデータで十分に効果的であることを示そうとするものです。この論文のマイクロソフトの研究者たちは、GPT-3.5とGPT-4から生成された合成データを使用しましたが、これには約$10,000の価格がかかるでしょう。データセットとモデルは、著者のHuggingFaceリポジトリから入手できます。

30M以下のパラメータでモデルをトレーニングできるというアイデアに魅了されました。参考までに、私はLenovo Legion 5ラップトップ(GTX 1660 Ti搭載)でモデルのトレーニングと推論をすべて実行しています。推論にしても、30億以上のパラメータを持つほとんどのモデルは私のマシンでは実行するには大きすぎます。クラウドコンピューティングリソースは価格に応じて利用できるとは思いますが、私はこれを余暇の時間に学んでおり、APIコールによって蓄積されるささやかなOpenAIの請求金額しか負担できません。したがって、私の控えめなハードウェアでトレーニングできるモデルがあるというアイデアは、私を一瞬にして興奮させました。

私はTinyStoriesの論文を読み始め、彼らが廃止されたGPT Neoモデルをモデルトレーニングに使用したことに気付きました。私はコードを探求して理解できるかどうかを確認するために探求を始め、より小さなものが必要だと気付きました。文脈を考えると、私は主にバックエンドソフトウェアエンジニアであり、ニューラルネットワークについて話す人々が話すときに完全に迷わない程度の機械学習の経験しかありません。私はまったく適切なMLエンジニアではないため、「ゼロからgpt」と入力して私の選択した検索エンジンでより優しい紹介を見つけました。下のビデオを見つけ、すべてが変わりました。

これは私が探していたものでした。ビデオにリンクされた基本的なリポジトリに加えて、まだ開発中の洗練されたバージョンであるnanoGPTもあります。さらに、トレーニングコードとモデルコードはそれぞれ約300行のPythonで書かれています。それは、私にとってビデオよりもさらに興奮することでした。私はビデオを閉じてソースコードを熟読し始めました。nanoGPTはPyTorchを使用しており、私はそれを以前使ったことがありません。さらに、初心者の私にとっては十分な数学と機械学習の専門用語があり、それが私にとって予想以上の大きな作業になることがわかりました。

何かを理解するためには、それについて書くことが最善の方法の一つです。したがって、私はnanoGPTリポジトリのコードを解析し、有名な「Attention is All You Need」の論文を読み、トランスフォーマーをボトムアップで実践的な方法で学ぶつもりです。途中で学んだことは、このシリーズで書き留めるつもりです。もし一緒に進んでいただけるのであれば、nanoGPTリポジトリを自分のマシンにクローン(モデルはCPUでトレーニングすることもできますので、ハードウェアの言い訳はありません)し、一緒に進んでください。

リポジトリをクローンした後、READMEに従って最も単純なモデルである文字レベルの生成モデルのトレーニングを行いました。トレーニングのためのデータセットの準備スクリプト、実際のトレーニングを行うスクリプト、生成されたテキストを出力するサンプリングスクリプトがあります。いくつかのターミナルコマンドと1時間以上のトレーニングの後、シェイクスピア風のテキストを出力する単純なモデルを手に入れました。

指示に従うことは良いことですが、私は自分のユースケースに適用するためにそれを変更するまで本当に理解していません。私の目標は、TinyStoriesデータセットを使用して同様の文字レベルモデルをトレーニングすることでした。これには、トレーニングのためのデータセットを準備するための独自のスクリプトを作成する必要がありました。それについて詳しく調べてみましょう。

nanoGPTには、GPT-2スタイルのモデルと文字レベルのモデル用の2種類のデータ準備スクリプトがあります。私はGPT-2モデルの一部のコードを使用してHuggingFaceリポジトリからダウンロードし、残りのすべてをtiny_shakespeareの文字レベルスクリプトから取得しました。重要なポイントは、tiny_shakespeareはわずか1MB余りであり、40,000行しかシェイクスピアのテキストが含まれていません。TinyStoriesは圧縮されて3GB以上あり、39.7Mの物語が含まれています。tiny_shakespeareのトークン化とスライシングの方法は直接転用できませんでした、少なくとも私のラップトップが32GBのRAMを持っている限りは。私はPythonicで読みやすいTinyStoriesの準備方法を試したときに何度もマシンをクラッシュさせました。最終的なスクリプトでは、以下で詳細に説明しますが、いくつかのトリックを使用しています。

まず、データのリストを処理するための私の好ましい解決策は、リスト内包表記です。これは既存のリストから変更を加えて新しいリストを生成するための構文です。ただし、この場合、問題は3GBの圧縮テキストがRAM上で10GBに近づくことです。リスト内包表記は、RAM上で複数のリストのコピーを必要とします。小さいデータには問題ありませんが、TinyStoriesの場合は使えません。

データの準備スクリプトの出力は、トレーニングデータと検証データの文字レベルのエンコーディングの圧縮されたNumPy配列と、メタデータのピクル(これには一意の文字のリストとこれらの文字を数値に変換するためのエンコーディング/デコーディングマップが含まれます)です。これを参照して、一度一意の文字が見つかり数値にマッピングされたら、私たちは最終的なエンコードされた数値の配列以外は何も必要としません。これをメモリ効率良く行う最良の方法は、これらの出力を一つずつ構築しながらデータを単純なforループで反復処理することです。これには、ループの前に初期変数を初期化し、それが各反復ごとに更新されるようにする必要があります。これにより、データセットの複数のバージョンがRAMに保持されず、必要なもののみが出力されます。最終的な語彙生成コードは以下の通りです:

chars_dataset = set([])len_dataset = 0# このテキストに出現するすべての一意の文字を取得し、トレーニングデータの総文字数を取得するdesc = "トレーニングセットの文字を列挙する"for story in tqdm(dataset['train']['text'], desc):    chars = list(set(story))    for char in chars:        chars_dataset.add(char)        len_dataset += len(story)

とはいえ、数値としてエンコードされた30.7Mのストーリー(40億文字以上)の配列は、Pythonが整数を動的に格納しているため、かなりの量のRAMを使用します。そこで登場するのが、NumPyです。NumPyは、整数の正確なサイズを指定できるより効率的な配列ストレージを持っています。効率的なストレージに加えて、NumPyには一度にすべてを構築するのではなく、反復的に最終的なエンコードされた配列を構築するためのメモリ効率の高い配列連結もあります。

スクリプトの最後の仕上げとして、各ステップに進捗バーを追加するためにtqdmを使用し、ついにスクリプトを実行する準備が整いました。そのため、夜通し実行し、朝戻ってきました。戻ってきたとき、スクリプトはまだ実行中で、推定された計算時間が100時間以上残っていました。

これが本当に私に衝撃を与えた瞬間でした。30.7Mのストーリーは言語モデルにとっては小さいですが、単一のスレッドで処理するためのおもちゃのデータセットではありません。大砲を持ち込む時が来ました:並列処理。並列処理には多くの複雑さとオーバーヘッドが伴いますが、パフォーマンスの向上はそのトレードオフに値するものでした。幸い、Pythonコードを並列化する方法はいくつかあります。これらの解決策の多くは、直列で実行されるスクリプトを大幅に書き換えるか、複雑な抽象化が必要です。少し調べてみると、スクリプトのほとんどを同じままにしておきながら、複数のプロセスを実行してすべてのスレッドを活用することができるものを見つけました。

RayはPythonでメソッドを簡単に並列化するためのライブラリであり、ローカルまたはクラスタとして簡単に実行できます。Rayはキューでタスクを実行し、そのキューからワーカープロセスを起動して処理します。以下は、rayによって初めて私のコードベースで表示された見本です:

import rayray.init()…# データセット内のすべての一意の文字が与えられた場合、# 文字と整数の一意のマッピングを作成しますstoi = { ch:i for i,ch in enumerate(chars_dataset) }@ray.remotedef encode(s):    return [stoi[c] for c in s]…encoded_stories = []for story in dataset[‘train’][‘text’]:    encoded_stories.append(encode.remote(story))ray.get(encoded_stories)…

すべてのCPUのパワーを武装して前進しましたが、すぐにラップトップがクラッシュしました。rayが使用するローカル分散コールスタックでは、データセット全体が何度もメモリに展開されました。データセット全体を単純にキューにエンキューすると、メモリ不足エラーが発生しました。イライラして、これをRAMを増やすための言い訳に使いました(64GBが来ました!)、しかしRAMが届く間もコードを微調整し続けました。

次の論理的な手順は、Rayによって処理されるリクエストをメモリに収まるようにバッチ処理することでした。バッチ処理ロジックの追加は比較的簡単で、記事の最後にリンクを貼りますが、それが実際に興味深くなったのは、バッチサイズの実験を行ったときです。最初にランダムなバッチサイズ(5000)を選び、うまく始まりましたが、各バッチごとにシングルスレッドのコードに多くの時間が費やされていることが明らかになりました。

基本的に、好きなシステムモニターを見ていると、数分間ずっと1つのコアが使用され、最終的にラップトップのすべてのコアが数秒間点灯した後、また1つのコアだけが使用されるという状態が見えました。これを改善するために、バッチサイズを微調整することにしました。バッチサイズを下げても助けにはなりませんでした。なぜなら、各バッチにはフルデータセットからスライスしてバッチを準備するための同期コードが多く含まれていたからです。このコードは並列化できなかったため、各バッチにはチャンクを生成するための大きなスタートアップコスト(時間的に)がかかりました。それで、逆のアプローチを試してみることにしました。チャンクサイズを増やして、コアをより長い間活用するようにしました。これはうまく機能しました。チャンクの生成にかかる時間はチャンクサイズにかかわらず同じでしたが、それぞれのチャンクはより多くのデータを処理しました。さらに、エンコーディングの後処理をRay関数に移動させることで、たった数時間でトレーニングデータセットの30%を処理することができました。しかも、これは1台のラップトップ上でのことです。

数時間後、文字レベルモデルに供給するための完全に準備されたカスタムデータセットを手に入れました。トレーニングセットを処理するために高価なクラウドコンピュートを利用する必要はなかったため、満足しています。さらに、文字レベルモデルのデータセットの作成/処理がどのようなものかを詳しく学びました。

このシリーズの次の記事では、実際のモデルコードを調査し、自分の知識が足りない場所では詳しい外部リソースへのリンクを提供します。記事が書かれたら、ここにリンクを追加します。その間、以下にデータセットの準備スクリプトの最終バージョンへのリンクを貼っておきましたので、ご参照いただき、限られた計算プラットフォーム上で比較的大きなデータセットを処理するために必要な手順をご覧いただけます。

nanoGPT/data/tinystories_char/prepare.py at master · oaguy1/nanoGPT

The simplest, fastest repository for training/finetuning VoAGI-sized GPTs. – nanoGPT/data/tinystories_char/prepare.py…

github.com

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more