2
3

小さなGPUでのエンジニアリング。TensorFlow.js での AIモデルゲーム。

Last updated at Posted at 2024-08-30

ショートストーリー: テンソルの彼方に

東京の下町に住むプログラマー、田中健一は、言語モデルのAIを開発するスタートアップ企業で働くエンジニアだった。彼の仕事は、複雑なアルゴリズムとデータ処理に囲まれた毎日で、言語モデルがどのようにして知識を構築し、自然な言葉を生成するのかを深く探求していた。

ある日、健一はプロジェクトの一環として、4次元テンソルを使った新しい計算モデルの設計を任された。これまでは2次元や3次元のテンソルを扱ってきたが、4次元のテンソルは彼にとって未知の領域だった。彼はテンソルの奥深さに魅了され、挑戦する決意を固めた。

健一は、言語モデルの背後に潜む計算プロセスに思いを馳せた。単語がエンベディングされてベクトルとなり、それらのベクトルが連なって行列を形成する。さらに、その行列が複数連なり、3次元テンソルを構成する。そして、この3次元テンソル同士の掛け算を行うことで、コンテキストテンソルが得られ、その中から意味がある行列を取り出すことができる。この行列をテキストに変換すれば、言語モデルの応答が得られるのだ。健一は、このシンプルなテンソルの掛け算が言語モデルの本当の姿であると気づいた。

スクリーンショット 2024-08-30 142529.png

さらに、健一はMOE(Mixture of Experts)という新しいAIモデルの構想に興奮した。MOEは複数のエキスパートが一つのAIの中に存在し、それぞれが専門的な知識を持っている。これを実現するためには、4次元テンソル同士の掛け算を行うことで、各エキスパートが異なる視点で情報を処理できることに気づいた。これにより、AIの理解力が飛躍的に向上するのだ。

健一は、3次元テンソルを使用して計算を行うためのコードを書き終え、キャンバスに結果を描画するプログラムを実装した。テンソル計算で表現された言語モデルを構築しテキスト生成を行った。彼は、テンソル計算が言語モデルの核心であることを再確認し、これによってAIの深い理解力を実現するための道が開けると確信した。

スクリーンショット 2024-08-30 143602.png

仕事を終えた健一は、夜の東京の街を歩きながら、自分の仕事の重要性を改めて感じた。テンソル計算が、ただの数値の操作に過ぎないように見えても、それがAIの高度な知識と理解を支えているのだと実感した。彼の心には、テンソルの彼方に広がる無限の可能性が広がっていることを感じながら、次の挑戦に向けて新たなコードを書き始めた。

TensorFlow.js を使用した テキスト生成ゲームです。

小さなGPUでの並列計算で、テンソル掛け算を行ってます。

スクリーンショット 2024-08-30 142507.png

生成テキストは英語ですが、ブラウザの翻訳機能で翻訳できます。

この計算プロセスは、Transformerのような言語モデルが行うアテンションメカニズムをシンプルに模倣したものです。以下のポイントで言語モデルの動作に対応しています:

ベクトル化とテンソル表現:

各単語がエンベディングによりベクトル化され、そのベクトルが連なって一つのテキストとして行列で表現されます。複数のテキストが存在するため、これらが積み重なり、最終的に3次元テンソルとして表現されます。

重みテンソルとの掛け算:

3次元の入力テンソル(テキストA)に対して、3次元の学習済みの重みテンソル(ここではシンプルにテキストBのテンソル化したもの)と掛け算を行います。これは、アテンションメカニズムにおける「クエリ」「キー」「バリュー」テンソルの操作に対応し、内積を通じて類似度や重要度を計算するプロセスです。

コンテキストテンソル(アテンションバリュー)の生成:

掛け算によって得られたテンソルCは、文脈情報を保持しており、これはアテンションメカニズムで得られるコンテキストベクターに対応します。このテンソルCには、各単語が文脈においてどのような意味を持つかの情報が含まれています。

スライスによるテキスト生成:

テンソルCから特定のスライスを取り出すことで、生成されるべきテキスト、つまり次の出力が得られます。これは、言語モデルが次の単語を予測する際に、コンテキストから適切な出力を生成するプロセスと類似しています。

この計算モデルを通じて、単語のエンベディングから始まり、アテンションを経てコンテキストを得て、最終的に次の単語やテキストを生成する一連の流れをシンプルに再現しています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TensorFlow.jsによるテキスト生成</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            padding: 20px;
        }
        #result {
            margin-top: 20px;
            font-size: 18px;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <h1>Text generate</h1>
    <button id="generateButton">Text generate</button>
    <div id="result"></div>

    <script>
        async function generateText() {
            // サンプルテキスト
            const textA = "Kenji, a young programmer living in downtown Tokyo, was performing amazing experiments with his small laptop. Kenji was passionate about computer science and was especially interested in computing with GPUs. Today, he was determined to simulate a 'quantum entangled state' with his small GPU.";
            const textB = "I’ll simulate all the possibilities of bit strings! Kenji was enthusiastic. He had recently been fascinated by the concept of quantum bits, learning about the state in which all combinations of bits overlap at the same time. With this knowledge, he wrote a program to mimic this entangled state using his laptop. Kenji's laptop has a built-in GPU, and he wanted to take advantage of its performance.";
            
            // テキストのトークン化
            const tokenize = text => text.toLowerCase().split(/\s+/);

            const tokensA = tokenize(textA);
            const tokensB = tokenize(textB);

            // プレースホルダーとしての単語ベクトル
            const vectorSize = 64;
            const vectorsA = Array(tokensA.length).fill().map(() => Array(vectorSize).fill(Math.random()));
            const vectorsB = Array(tokensB.length).fill().map(() => Array(vectorSize).fill(Math.random()));

            // 配列をテンソルに変換
            const tensorA = tf.tensor2d(vectorsA);
            const tensorB = tf.tensor2d(vectorsB);

            // 行列乗算
            const result = tf.matMul(tensorA, tensorB.transpose());

            // ランダムな行を取得
            const randomIndex = Math.floor(Math.random() * result.shape[0]);
            const randomSlice = result.slice([randomIndex, 0], [1, result.shape[1]]).flatten().arraySync();

            // 温度付きサンプリング関数
            function sampleWithTemperature(probs, temperature = 1.0) {
                probs = probs.map(p => Math.log(p + 1e-10) / temperature);
                const expProbs = probs.map(Math.exp);
                const sumExpProbs = expProbs.reduce((a, b) => a + b, 0);
                const normalizedProbs = expProbs.map(p => p / sumExpProbs);
                const rand = Math.random();
                let cumulative = 0;
                for (let i = 0; i < normalizedProbs.length; i++) {
                    cumulative += normalizedProbs[i];
                    if (rand < cumulative) return i;
                }
                return normalizedProbs.length - 1;
            }

            // 単語インデックスを生成
            const generatedIndices = [];
            for (let i = 0; i < 20; i++) {
                const probs = Array(randomSlice.length).fill().map(() => Math.random());
                const index = sampleWithTemperature(probs, 1.0);
                generatedIndices.push(Math.floor(index) % tokensA.length);
            }

            // トークンリストから単語を取得
            const generatedWordsA = generatedIndices.map(i => tokensA[i] || '<UNK>').join(' ');
            const generatedWordsB = generatedIndices.map(i => tokensB[i] || '<UNK>').join(' ');

            // 結果を表示
            document.getElementById('result').innerText = `Text generate A:\n${generatedWordsA}\n\nText generate B:\n${generatedWordsB}`;
        }

        // ボタンがクリックされたときにテキスト生成を呼び出す
        document.getElementById('generateButton').addEventListener('click', generateText);
    </script>
</body>
</html>

pythonコードでのより精密なテキスト生成。

実行結果。

Generated Text A: small programmer amazing , computer state small with was tokyo young with and experiments in his . tokyo computing , science kenji a computer gpu a about . programmer with
Generated Text B: had all was ’ of this wrote . ! bit simulate . bits enthusiastic the he all bit state strings quantum i ll of a time concept been all .

生成されたテキスト A: 小さなプログラマーは驚くべきもので、コンピューターの状態は小さく、東京の若いプログラマーは彼の実験をしていました。東京のコンピューティング、科学のケンジはコンピューターの GPU についてです。プログラマーは
生成されたテキスト B: すべてはこれについて書きました。! ビットをシミュレートします。ビットは熱狂的で、彼はすべてのビット状態文字列の量子の時間の概念をすべて持っていました。

import tensorflow as tf
import numpy as np
from gensim.models import Word2Vec
from nltk.tokenize import word_tokenize
import nltk
import random

nltk.download('punkt')

# サンプルテキスト
text_A = ("Kenji, a young programmer living in downtown Tokyo, was performing amazing experiments with his small laptop. "
          "Kenji was passionate about computer science and was especially interested in computing with GPUs. Today, "
          "he was determined to simulate a 'quantum entangled state' with his small GPU.")

text_B = ("I’ll simulate all the possibilities of bit strings! Kenji was enthusiastic. He had recently been fascinated "
          "by the concept of quantum bits, learning about the state in which all combinations of bits overlap at the same time. "
          "With this knowledge, he wrote a program to mimic this entangled state using his laptop. Kenji's laptop has a built-in GPU, "
          "and he wanted to take advantage of its performance.")

# トークン化
tokens_A = word_tokenize(text_A.lower())
tokens_B = word_tokenize(text_B.lower())

# Word2Vecモデルのトレーニング
model_A = Word2Vec([tokens_A], vector_size=64, window=5, min_count=1, workers=4)
model_B = Word2Vec([tokens_B], vector_size=64, window=5, min_count=1, workers=4)

# トークンを64次元ベクトルに変換
vectors_A = np.array([model_A.wv[word] for word in tokens_A])
vectors_B = np.array([model_B.wv[word] for word in tokens_B])

# 64次元のベクトルをテンソルに変換
tensor_A = tf.convert_to_tensor(vectors_A, dtype=tf.float32)
tensor_B = tf.convert_to_tensor(vectors_B, dtype=tf.float32)

# 行列乗算を行う
result = tf.matmul(tensor_A, tf.transpose(tensor_B))

# ランダムな行列を1枚スライス
random_index = random.randint(0, result.shape[0] - 1)
random_slice = result[random_index]

# テンパレーチャで単語ベクトルを選択する関数
def sample_with_temperature(probs, temperature=1.0):
    probs = np.log(probs + 1e-10) / temperature  # 防御的な対策として0割り防止
    exp_probs = np.exp(probs)
    probs = exp_probs / np.sum(exp_probs)
    return np.random.choice(len(probs), p=probs)

# 30個の単語ベクトルを選択
generated_indices = []
for _ in range(30):
    # 確率分布の作成
    probs = np.random.rand(random_slice.shape[0])
    index = sample_with_temperature(probs, temperature=1.0)
    # インデックスが有効な範囲に収まることを確認
    index = int(index) % len(tokens_A)
    generated_indices.append(index)

# リストの範囲内でインデックスを使用
generated_words_A = [tokens_A[i] if i < len(tokens_A) else '<UNK>' for i in generated_indices]
generated_words_B = [tokens_B[i] if i < len(tokens_B) else '<UNK>' for i in generated_indices]

generated_text_A = ' '.join(generated_words_A)
generated_text_B = ' '.join(generated_words_B)

print("Generated Text A:", generated_text_A)
print("Generated Text B:", generated_text_B)

計算モデルの説明

3次元テンソルの生成:

tf.random.normal関数を使用して、ランダムな値を持つ2つの3次元テンソル(tensor_a と tensor_b)を生成します。各テンソルの形状は (10, 10, 10) です。

テンソル同士の掛け算:

tf.tensordot関数を使用して、テンソル同士の掛け算(テンソル積)を行います。ここでは、第3次元 (axes=[[2], [0]]) を基に内積を計算します。
result_tensorは、形状 (10, 10, 10) の3次元テンソルになります。

スライスの取り出し:

result_tensorから、特定のスライスを取り出します。ここでは、第3次元の最初のスライス [:, :, 0] を取り出しています。このスライスは、2次元のテンソル (10, 10) になります。

結果の確認:

スライスした2次元テンソルの形状や中身を表示しています。

実行結果のイメージ
Result Tensor Shape: (10, 10, 10)
Slice Tensor Shape: (10, 10)
Slice Tensor Values: は (10, 10) の行列として表示されます。

このコードは、3次元テンソル同士の演算をシンプルに実行し、その結果から2次元のコンテキストを取り出すというタスクを実現しています。

追記。

A と行列 B の掛け算によってアテンションウェイトを計算する場合、行うのは**マトリックス積(行列積)**であり、これは「マットムル(matmul)」とも呼ばれます。

アテンションウェイトの計算

アテンションメカニズムにおいては、行列積が頻繁に使用されます。具体的には、クエリ(Query)行列とキー(Key)行列の行列積をとり、その結果にソフトマックス関数を適用してアテンションウェイトを計算します。
行列積を使用する理由は、ベクトルや行列の空間的な関係を保持しながら、要素間の関連性を効率的に計算できるからです。

ドット積との違い

ドット積の場合、結果として得られるのはスカラー値(1つの数値)です。これは、行列同士の関係を計算するには適していません。
行列積では、行列同士の要素間の関係を保ちながら、複数の関連性を一度に計算できます。
したがって、アテンションウェイトを計算する場合は、**行列積(matmul)**を使用するのが正しい方法です。
ドット積で得られる値は、二つのベクトル(行列の特定の行や列がベクトルとして扱われる場合)の類似度を示すスカラー値です。

ドット積の特徴
類似度: ドット積は、二つのベクトルがどれだけ「同じ方向」を向いているか、つまりどれだけ類似しているかを表す指標となります。ドット積が大きいほど、二つのベクトルは類似していることを意味します。
スカラー値: ドット積の結果はスカラー値であり、これは単一の数値です。この数値は、二つのベクトル間の内積(すなわち、類似度)を表します。

まとめ

ドット積: ベクトル間の類似度を表すスカラー値を返す。
行列積: 二つの行列間のより複雑な関係(例えば、アテンションウェイトなど)を計算するためのもので、結果として新しい行列を返す。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3