2
3

TensorFlow.js 「Universal Sentence Encoder」エンベディングを用いたテキスト生成のニューラルネットワークモデル。

Last updated at Posted at 2024-09-17

ショートストーリー「小さなGPUで広がる世界」

都内の小さなワンルームで、主人公の高橋は朝からキーボードを叩いていた。彼はフリーランスのプログラマーで、日々の仕事は多岐にわたる。そんな彼が最近、頭を悩ませていたのが「小さなGPUで、どれだけ高性能なテキスト生成モデルを作れるか」という挑戦だった。

高橋の机の上には、内蔵GPUを持つ小型のコンピュータが置かれていた。外から見ればただの小さなデバイス。しかし、高橋にとってこの小さなコンピュータは、広大なテキスト生成の世界を探求するための旅の道具だった。

「よし、まずはエンベディングから始めよう。」

彼は、すでに事前学習済みの「Universal Sentence Encoder」を使い、テキストをベクトルに変換するコードを書き始めた。エンベディングはテキスト生成モデルにおいて重要なステップだ。テキストの持つ意味や文脈をベクトルとして表現することで、モデルが人間のように言葉を理解し、次に続く単語を予測できるようになる。だが、問題はここからだった。この小さなGPUで、果たして高精度なモデルを作ることができるのか。

高橋は考えた末、シンプルなニューラルネットワークモデルを設計することにした。2層の全結合層のみの構造で、入力データとして3つの単語のエンベディングを連結し、次の単語のベクトルを予測するものだ。複雑な構造にせず、軽量で効率的なモデルにすることが成功の鍵だと考えたのだ。

「ターゲットデータは次の単語か…よし、この形でトレーニングしてみよう。」

彼は、何度もコードを修正し、モデルのトレーニングを繰り返した。小さなGPUは高性能とはいえないが、その限られたリソースでいかに効率よく学習させるかが高橋の腕の見せ所だった。トレーニングデータには、エンベディングされたテキストデータを使い、予測精度を少しずつ高めていく。

やがて、モデルがある程度の精度で次の単語を予測できるようになった。ここで彼は、テキスト生成プロセスに一工夫を加えることにした。ただ最も類似度の高いベクトルを選ぶのではなく、上位10個の中からランダムに1つを選ぶ手法だ。これにより、生成されるテキストに多様性を持たせ、より自然な文章を生み出せると考えた。

「じゃあ、試しに生成してみようか。」

高橋は、初期入力として3つの単語を与え、生成を開始した。モデルは次々と次の単語を予測し、テキストを生成していく。その様子を見て、高橋の心に緊張と期待が入り混じる。そして、10回分のテキスト生成が終わり、スクリーンにはモデルが生み出した文章が表示された。

「おお、これは…!」

思わず声が漏れる。生成された文章は、確かに人間が書いたような自然な流れを持っていた。もちろん、まだ改善の余地はあったが、小さなGPUでもここまでのモデルを作ることができるという事実に、高橋は手応えを感じた。

「これで、次はもっと複雑な文脈にも対応できるように改良していこう。」

彼はさらなる挑戦に向けて、またキーボードを叩き始めた。小さな内蔵GPUであっても、その中には無限の可能性が広がっている。それを引き出すのは、彼自身の情熱と創造力だった。

「さあ、この小さなGPUで、どこまで広がる世界を見せてくれるか。」

高橋の目は、コンピュータのスクリーンの向こうに広がる未来を見つめていた。

実行結果。生成されたテキスト。小さなGPUでトレーニングを敢行。(意味 悪条件にもかかわらず、あえて押し切って行うこと)

スクリーンショット 2024-09-18 041527.png

実行結果。エポック200。

ある日、小太郎は、あることを思い出して、そのことについていろいろと考え、いろいろなことを試してみたが、結局、いろいろなことがわからなくなってしまった。彼は、あまり使っていなかった。そして、ほとんど使っていないものを見つけた。彼は、3年生のときに、IntelのCPUを搭載したコンピューターにしか乗らなかったと言った。父親は、彼が学校でゲームをするたびに、このCPUがもっと速く計算できると言い、最新のN100を買った。しかし、最近買ったN100はもっと速く計算できるだろうか?と、小太郎は思った。しかし、高価なコンピューターなので、お金がかかる。「この速度は、GPUを使って計算する能力がないと聞いたが、コンピューターはJavaScriptにアクセスできる。GPUライブラリは、TensorFlow.jsだ。WebGL関数。TensorFlow.jsは、行列乗算ができるとわかった!」と、興奮して言った。

実行結果。エポック600。

ある日、光太郎はパソコンの内蔵GPUを取り出し、ちょっとしたことでもパソコンの使い方を教えてもらった。小学校3年生の時に、彼はCPUにIntelのN100という機能があることを知った。最近、このCPUがゲーム用に高価なパソコンを買ったんだって。「このGPUならもっと速く計算できるんだ。みんな羨ましい?」と聞いた。しかし光太郎は、このパソコンのGPUを使っても、そのお金でプログラミングをすることはできず、その使い方しか方法がないことを知った。そこで、WebGLを使ってブラウザのアクセス速度を速める方法を思いついた。彼は興奮して言った。「TensorFlow.jsだ!行列APIを書き換えて掛け算の計算ができる関数を見つけたんだ。新しいプログラムが見つかったんだ」

実行結果。エポック1000。

ある日、幸太郎は内蔵GPUを取り外す。幸太郎は、自分のパソコンの作り方をいろいろ調べ、パソコンを買った。すると、いろいろなパソコンのCPUが使えることがわかった。すると、そこには「そんなの、ほとんどない!」と書いてあった。 困ったことに、幸太郎は、小学校3年生の時に買ったIntelのN100というCPUを買った。 最新のCPUは、もっと高速に計算できる。 真面目なゲーム好きの少年、幸太郎は、PCを学校に持っていく。 でも、毎月の友達は、このGPUを買うのがやっと。 「この高価なパソコンを買うと、計算が速くなるの?」 「でも、幸太郎は、このGPUを使ってどんな機能を試したか覚えていない。 効果的なプログラミングをうらやましがっている。 JavaScriptのプログラムを探していた幸太郎は、GPUライブラリを探していた。 TensorFlow.js。 ブラウザAPIは、WebGLを聞いた。 TensorFlow.jsは、行列をすばやく書き換えることができるかもしれない。 お金がかかったら、プログラムのアイデアが浮かぶ!」 掛け算を学んだ。

解説: TensorFlow.js 「Universal Sentence Encoder」エンベディングを用いたテキスト生成のニューラルネットワークモデル

  1. はじめに
    本研究では、事前学習済みのUniversal Sentence Encoder(USE)を用いてテキストのエンベディングを取得し、そのエンベディングをトレーニングデータとして利用したシンプルなニューラルネットワークモデルによるテキスト生成の手法を提案する。本手法では、入力データとして連続する3つのトークン(単語)のエンベディングベクトルを結合し、次の単語のエンベディングベクトルを予測するモデルを構築する。生成プロセスでは、出力ベクトルに類似したベクトルを候補として選び、その中からランダムに次の単語を選択する。これにより、より自然なテキスト生成を実現する。

繰り返しを防ぐ方法:
生成済みの単語を記録して、同じ単語を繰り返さないようにします。
ランダム性を高めるために、上位10個からランダムに選択する代わりに、確率分布に基づいて単語を選ぶ方法を利用します。

  1. モデル構築
    本モデルは、入力データとしてエンベディングベクトル(512次元で出力される)を利用するということで、事前学習済みのモデルをトレーニングデータ生成に活用している。構築されるニューラルネットワークは、以下の2層の全結合層(Fully Connected Layers)から成る。

隠れ層: 512ユニットを持ち、活性化関数としてReLU(Rectified Linear Unit)を使用する。入力サイズは、3つのトークン(単語)のエンベディングベクトルを連結したサイズに等しい。
出力層: 次の単語のエンベディングベクトルを出力する。活性化関数は線形(Linear)を使用し、出力サイズはエンベディングベクトルの次元数に一致する。
最適化手法にはAdamオプティマイザを採用し、損失関数には平均二乗誤差(Mean Squared Error)を使用する。これにより、出力ベクトルとターゲットのエンベディングベクトルの間の誤差を最小化する。

  1. エンベディングの取得とデータ前処理
    テキスト生成の前処理として、まず入力テキストを単語ごとに分割し、Universal Sentence Encoderを用いて各単語のエンベディングベクトルを取得する。このエンベディングベクトルは、事前学習済みモデルにより高次元空間に埋め込まれたものであり、単語の意味的な情報を含む。この過程により、入力されたテキスト全体に対してエンベディングベクトルの集合が得られる。

次に、トレーニングデータを生成する。入力データとして、連続する3つの単語のエンベディングベクトルを連結し、ターゲットデータとして次の単語のエンベディングベクトルを設定する。この過程を、テキスト内の全単語に対してスライドしながら繰り返すことで、複数の入力データとターゲットデータのペアを作成する。

  1. モデルのトレーニング
    トレーニングでは、前述の入力データとターゲットデータを用いて、モデルが次の単語のエンベディングベクトルを予測するように学習を行う。具体的には、連結された3つの単語のエンベディングベクトルをモデルに入力し、次の単語のエンベディングベクトルを出力する。この出力ベクトルとターゲットのエンベディングベクトルとの間の平均二乗誤差を最小化するように、Adamオプティマイザによりモデルの重みを更新する。このプロセスを全データに対して複数エポックにわたり繰り返すことで、モデルの予測精度を高める。

  2. テキスト生成
    トレーニングが完了したモデルを用いてテキスト生成を行う。生成プロセスは以下のステップから構成される。

初期入力: 生成を開始するために、最初の3つの単語を選択し、そのエンベディングベクトルを連結してモデルに入力する。
ベクトルの予測: モデルは次の単語のエンベディングベクトルを出力する。
類似度計算: 出力ベクトルと事前に取得しておいた全単語のエンベディングベクトルとの類似度を計算する。類似度の計算には、コサイン類似度を使用する。
単語の選択: 類似度の高い順にベクトルを並べ、上位10個を選択する。その中からランダムに1つの単語を選択し、次の単語として追加する。
繰り返し: 生成した単語を用いて次の入力データを更新し、同様のプロセスを繰り返す。これにより、50トークンのテキストが生成される。

繰り返しを防ぐ方法:
生成済みの単語を記録して、同じ単語を繰り返さないようにします。
ランダム性を高めるために、上位10個からランダムに選択する代わりに、確率分布に基づいて単語を選ぶ方法を利用します。

  1. 考察
    本手法では、事前学習済みのUniversal Sentence Encoderを用いることで、単語の意味的な情報を保持したままテキスト生成を行うことが可能である。また、次の単語の選択において、類似度からの確率分布に基づいて単語を選ぶ方法を利用する手法を用いることで、テキスト生成における多様性と自然性を実現している。これにより、完全に確率的な生成よりも文脈に適合し、かつ単調でないテキストを生成することが可能となる。

以上のように、Universal Sentence Encoderを用いたエンベディングベクトルに基づくニューラルネットワークモデルは、シンプルな構造でありながら、文脈に沿ったテキスト生成を実現する有効な手法である。

TensorFlow.js 「Universal Sentence Encoder」エンベディングを用いたテキスト生成のニューラルネットワークモデルのコード。

<!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>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/universal-sentence-encoder"></script>
</head>
<body>
    <h1>テキスト生成 - TensorFlow.js</h1>
    <div>
        <label for="input-text">テキストを入力してください:</label>
        <input type="text" id="input-text" size="50">
        <button id="train-button">モデルをトレーニング</button>
        <button id="generate-button">テキスト生成</button>
    </div>
    <div>
        <h3>生成されたテキスト:</h3>
        <pre id="generated-text"></pre>
    </div>

    <script>
        let model;
        let useModel;
        let embeddingsData = [];
        let vocabulary = [];

        // モデルを構築する関数。隠れ層を大きくして、学習能力を向上させる
        function buildModel(inputSize, outputSize) {
            const model = tf.sequential();
            model.add(tf.layers.dense({units: 512, activation: 'relu', inputShape: [inputSize]})); // 隠れ層1:ユニット数を増やす
            model.add(tf.layers.dense({units: 256, activation: 'relu'})); // 隠れ層2:もう一つ隠れ層を追加
            model.add(tf.layers.dense({units: outputSize, activation: 'linear'})); // 出力層
            model.compile({optimizer: 'adam', loss: 'meanSquaredError'}); // モデルのコンパイル
            return model;
        }

        // モデルをトレーニングする関数
        async function trainModel() {
            useModel = await use.load(); // Universal Sentence Encoderをロード
            const inputText = document.getElementById('input-text').value; // 入力されたテキストを取得
            vocabulary = inputText.split(' '); // 単語に分割して語彙リストを作成

            embeddingsData = [];
            for (const word of vocabulary) {
                const embedding = await useModel.embed([word]); // 各単語の埋め込みを取得
                embeddingsData.push(embedding.arraySync()[0]); // 埋め込みを配列に追加
            }

            const inputEmbeddings = [];
            const targetEmbeddings = [];

            // 3単語の組み合わせを入力として、次の単語を予測するためのデータセットを作成
            for (let i = 0; i < embeddingsData.length - 3; i++) {
                const inputVec = embeddingsData.slice(i, i + 3).flat(); // 入力ベクトル(3単語分)
                const targetVec = embeddingsData[i + 3]; // ターゲットベクトル(次の単語)
                inputEmbeddings.push(inputVec);
                targetEmbeddings.push(targetVec);
            }

            // モデルの構築
            model = buildModel(inputEmbeddings[0].length, targetEmbeddings[0].length);

            // データをテンソルに変換
            const xs = tf.tensor2d(inputEmbeddings);
            const ys = tf.tensor2d(targetEmbeddings);

            // モデルをトレーニング
            await model.fit(xs, ys, {
                epochs: 200, // エポック数を指定
                verbose: 1 // トレーニング中の出力を表示
            });

            xs.dispose();
            ys.dispose();
            alert('モデルのトレーニングが完了しました。');
        }

        // コサイン類似度を計算する関数
        function cosineSimilarity(vecA, vecB) {
            const dotProduct = tf.dot(vecA, vecB).arraySync(); // ベクトルの内積
            const magnitudeA = tf.norm(vecA).arraySync(); // ベクトルAの大きさ
            const magnitudeB = tf.norm(vecB).arraySync(); // ベクトルBの大きさ
            return dotProduct / (magnitudeA * magnitudeB); // コサイン類似度を返す
        }

        // テキストを生成する関数
        async function generateText() {
            if (!model || embeddingsData.length === 0) {
                alert('まずテキストを入力してモデルをトレーニングしてください。');
                return;
            }

            let generatedText = vocabulary.slice(0, 3).join(' '); // 初期テキスト
            let currentInput = embeddingsData.slice(0, 3).flat(); // 初期入力
            let generatedWords = new Set(vocabulary.slice(0, 3)); // 生成済みの単語を記録

            // 最大150単語まで生成
            for (let i = 0; i < 150; i++) {
                const inputTensor = tf.tensor2d([currentInput]); // 現在の入力をテンソルに変換
                const predictedEmbedding = model.predict(inputTensor).arraySync()[0]; // 次の単語の埋め込みを予測

                // 予測された埋め込みと語彙の各単語の埋め込みとの類似度を計算
                const similarities = embeddingsData.map(vec => cosineSimilarity(tf.tensor1d(predictedEmbedding), tf.tensor1d(vec)));
                const topIndices = similarities
                    .map((sim, index) => ({sim, index})) // 類似度とインデックスのペアを作成
                    .sort((a, b) => b.sim - a.sim) // 類似度でソート
                    .filter(item => !generatedWords.has(vocabulary[item.index])) // 生成済みの単語を除外
                    .slice(0, 10) // 類似度の高い上位10個を取得
                    .map(item => item.index);
                const randomIndex = topIndices[Math.floor(Math.random() * topIndices.length)]; // ランダムに次の単語を選択

                const nextWord = vocabulary[randomIndex]; // 次の単語を取得
                generatedText += ' ' + nextWord; // 生成されたテキストに追加
                generatedWords.add(nextWord); // 生成済み単語に追加

                // 次の入力を更新
                currentInput = currentInput.slice(embeddingsData[0].length).concat(embeddingsData[randomIndex]);
                inputTensor.dispose(); // テンソルを解放
            }

            // 生成されたテキストを表示
            document.getElementById('generated-text').innerText = generatedText;
        }

        // ボタンのイベントリスナーを設定
        document.getElementById('train-button').addEventListener('click', trainModel);
        document.getElementById('generate-button').addEventListener('click', generateText);
    </script>
</body>
</html>

トレーニングデータ。

ある日、小学3年生の男の子、光太郎はコンピュータの前で真剣な表情をしていました。 光太郎の使っているコンピュータは、インテルの最新N100CPUを搭載したものでした。このN100CPUには、内臓GPUという機能がついています。お父さんが「これで十分だよ」と言って買ってくれたものでしたが、光太郎は最近、もっと速く計算できるコンピュータが欲しいと思うようになりました。 学校の友達たちは、最新のゲーミングPCを持っている子もいて、彼らの話を聞くたびに、光太郎は少し羨ましく思いました。でも、光太郎のお小遣いは毎月少しだけで、そんな高いコンピュータを買うお金はありませんでした。 「どうにかして、このコンピュータで高速に計算を行う方法はないかな?」 光太郎は、コンピュータの内蔵GPUを使って計算速度を上げる方法を模索していました。せっかくGPUが付いているのに、その機能を有効に活用する手段がわからず、困っていたのです。光太郎は、様々なプログラムを試みるものの、GPUの性能を引き出すことができずにいました。 そんなある日、光太郎はプログラミングの本棚からJavaScriptの本を取り出しました。すると、TensorFlow.jsというライブラリを使えば、ブラウザ上でGPUにアクセスできることを知りました。TensorFlow.jsは、WebGLというAPIを通じて、ブラウザから内蔵GPUにアクセスできるというのです。 「これだ!このアイデアだ!」と光太郎は興奮しました。これなら、内蔵GPUを有効に活用する方法が見つかるかもしれないと感じました。光太郎はすぐにTensorFlow.jsを使って、内蔵GPUを使った行列の掛け算プログラムを書き直しました。 光太郎が新しいプログラムを実行すると、内蔵GPUを活用することで計算が格段に速くなったことがわかりました。「やった!これなら、もっと速く計算できるかもしれない!」 光太郎は、自分のコンピュータに内蔵されているGPUを最大限に活用する方法を見つけ、これからもさまざまなプログラムに挑戦する決意をしました。高いコンピュータは買えなかったけれど、工夫と知識で、自分の手元にあるコンピュータを最大限に活かすことができたのです。

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