{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 生成ネットワーク\n", "\n", "リカレントニューラルネットワーク (RNN) とそのゲート付きセルのバリエーション(例えば、長短期記憶セル (LSTM) やゲート付きリカレントユニット (GRU))は、言語モデル化の仕組みを提供します。つまり、これらは単語の順序を学習し、シーケンス内の次の単語を予測することができます。この特性により、RNNを使用して、通常のテキスト生成、機械翻訳、さらには画像キャプション生成といった**生成タスク**を実行することが可能になります。\n", "\n", "前のユニットで議論したRNNアーキテクチャでは、各RNNユニットが次の隠れ状態を出力として生成していました。しかし、各リカレントユニットにもう一つの出力を追加することで、**シーケンス**(元のシーケンスと同じ長さのもの)を出力することが可能になります。さらに、各ステップで入力を受け取らず、初期状態ベクトルだけを受け取り、それに基づいて出力シーケンスを生成するRNNユニットを使用することもできます。\n", "\n", "このノートブックでは、テキスト生成を助けるシンプルな生成モデルに焦点を当てます。簡単のために、**文字レベルのネットワーク**を構築し、文字ごとにテキストを生成します。トレーニング中には、テキストコーパスを取得し、それを文字列のシーケンスに分割する必要があります。\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n", "Building vocab...\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "import numpy as np\n", "from torchnlp import *\n", "train_dataset,test_dataset,classes,vocab = load_dataset()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## キャラクター語彙の構築\n", "\n", "キャラクターレベルの生成ネットワークを構築するには、テキストを単語ではなく個々の文字に分割する必要があります。これを実現するためには、異なるトークナイザーを定義する必要があります。\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary size = 82\n", "Encoding of 'a' is 1\n", "Character with code 13 is c\n" ] } ], "source": [ "def char_tokenizer(words):\n", " return list(words) #[word for word in words]\n", "\n", "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " counter.update(char_tokenizer(line))\n", "vocab = torchtext.vocab.vocab(counter)\n", "\n", "vocab_size = len(vocab)\n", "print(f\"Vocabulary size = {vocab_size}\")\n", "print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n", "print(f\"Character with code 13 is {vocab.get_itos()[13]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "データセットからテキストをどのようにエンコードできるかの例を見てみましょう。\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n", " 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n", " 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n", " 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n", " 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n", " 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n", " 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n", " 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def enc(x):\n", " return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n", "\n", "enc(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ジェネレーティブRNNのトレーニング\n", "\n", "RNNを使ってテキストを生成する方法は以下の通りです。各ステップで、`nchars`の長さの文字列を入力として取り、ネットワークに対して各入力文字に対する次の出力文字を生成するように求めます。\n", "\n", "![単語 'HELLO' を生成するRNNの例を示す画像。](../../../../../translated_images/ja/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "実際のシナリオによっては、*end-of-sequence* `` のような特別な文字を含めることもあります。しかし、今回の場合は無限にテキストを生成するネットワークをトレーニングしたいので、各シーケンスのサイズを`nchars`トークンに固定します。その結果、各トレーニング例は`nchars`の入力と`nchars`の出力(入力シーケンスを1文字左にシフトしたもの)で構成されます。ミニバッチはこのようなシーケンスをいくつかまとめたものになります。\n", "\n", "ミニバッチを生成する方法としては、長さ`l`のニューステキストを取り、それから可能なすべての入力-出力の組み合わせを生成します(その組み合わせは`l-nchars`個になります)。これらは1つのミニバッチを形成し、トレーニングステップごとにミニバッチのサイズは異なることになります。\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n", " [ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " ...,\n", " [20, 8, 21, ..., 1, 28, 1],\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16]]),\n", " tensor([[ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " [ 2, 3, 4, ..., 1, 16, 26],\n", " ...,\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16],\n", " [ 5, 8, 9, ..., 27, 16, 6]]))" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "nchars = 100\n", "\n", "def get_batch(s,nchars=nchars):\n", " ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " for i in range(len(s)-nchars):\n", " ins[i] = enc(s[i:i+nchars])\n", " outs[i] = enc(s[i+1:i+nchars+1])\n", " return ins,outs\n", "\n", "get_batch(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ジェネレーター・ネットワークを定義してみましょう。これは、前のユニットで説明した任意のリカレントセル(シンプル、LSTM、またはGRU)を基に構築できます。この例ではLSTMを使用します。\n", "\n", "ネットワークは文字を入力として受け取り、語彙サイズが比較的小さいため、埋め込み層は必要ありません。一つのホットエンコードされた入力を直接LSTMセルに渡すことができます。ただし、文字の番号を入力として渡すため、LSTMに渡す前にそれらを一つのホットエンコードに変換する必要があります。これは、`forward`パス中に`one_hot`関数を呼び出すことで行われます。出力エンコーダーは、隠れ状態を一つのホットエンコードされた出力に変換する線形層になります。\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class LSTMGenerator(torch.nn.Module):\n", " def __init__(self, vocab_size, hidden_dim):\n", " super().__init__()\n", " self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n", "\n", " def forward(self, x, s=None):\n", " x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n", " x,s = self.rnn(x,s)\n", " return self.fc(x),s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "トレーニング中に生成されたテキストをサンプリングできるようにしたいと考えています。そのために、初期文字列 `start` から始まり、長さ `size` の出力文字列を生成する `generate` 関数を定義します。\n", "\n", "その動作は以下の通りです。まず、初期文字列全体をネットワークに通し、出力状態 `s` と次に予測される文字 `out` を取得します。`out` はワンホットエンコードされているため、`argmax` を使用して語彙内の文字 `nc` のインデックスを取得し、`itos` を使って実際の文字を特定し、結果として得られる文字列リスト `chars` に追加します。この1文字を生成するプロセスを `size` 回繰り返し、必要な文字数を生成します。\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def generate(net,size=100,start='today '):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " nc = torch.argmax(out[0][-1])\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "さあ、トレーニングを始めましょう!トレーニングループはこれまでの例とほぼ同じですが、精度を表示する代わりに、1000エポックごとに生成されたテキストのサンプルを表示します。\n", "\n", "損失の計算方法には特に注意が必要です。損失を計算するには、ワンホットエンコードされた出力 `out` と、期待されるテキスト `text_out`(文字インデックスのリスト)を使用します。幸いなことに、`cross_entropy` 関数は非正規化されたネットワーク出力を最初の引数として受け取り、クラス番号を2番目の引数として受け取ります。これはまさに私たちが持っているものです。この関数はミニバッチサイズに対する自動平均化も行います。\n", "\n", "また、トレーニングを `samples_to_train` サンプルで制限し、待ち時間を短縮します。ぜひ実験してみて、より長いトレーニングを試してみてください。場合によっては、複数のエポックでトレーニングすることも可能です(その場合、このコードの周りに別のループを作成する必要があります)。\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current loss = 4.398899078369141\n", "today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n", "Current loss = 2.161320447921753\n", "today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n", "Current loss = 1.6722588539123535\n", "today and the court to the could to the could to the could to the could to the could to the could to the c\n", "Current loss = 2.423795223236084\n", "today and a second to the conternation of the conternation of the conternation of the conternation of the \n", "Current loss = 1.702607274055481\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.692358136177063\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.9722288846969604\n", "today and the control the control the control the control the control the control the control the control \n", "Current loss = 1.8705692291259766\n", "today and the second to the second to the second to the second to the second to the second to the second t\n", "Current loss = 1.7626899480819702\n", "today and a security and a security and a security and a security and a security and a security and a secu\n", "Current loss = 1.5574463605880737\n", "today and the company and the company and the company and the company and the company and the company and \n", "Current loss = 1.5620026588439941\n", "today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n" ] } ], "source": [ "net = LSTMGenerator(vocab_size,64).to(device)\n", "\n", "samples_to_train = 10000\n", "optimizer = torch.optim.Adam(net.parameters(),0.01)\n", "loss_fn = torch.nn.CrossEntropyLoss()\n", "net.train()\n", "for i,x in enumerate(train_dataset):\n", " # x[0] is class label, x[1] is text\n", " if len(x[1])-nchars<10:\n", " continue\n", " samples_to_train-=1\n", " if not samples_to_train: break\n", " text_in, text_out = get_batch(x[1])\n", " optimizer.zero_grad()\n", " out,s = net(text_in)\n", " loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " if i%1000==0:\n", " print(f\"Current loss = {loss.item()}\")\n", " print(generate(net))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "この例はすでにかなり良いテキストを生成していますが、いくつかの方法でさらに改善することができます:\n", "\n", "* **より良いミニバッチ生成** \n", " トレーニング用のデータを準備する際、1つのサンプルから1つのミニバッチを生成する方法を採用しました。しかし、この方法は理想的ではありません。ミニバッチのサイズがすべて異なり、テキストが`nchars`より小さい場合にはミニバッチを生成できないこともあります。また、小さなミニバッチではGPUを十分に活用できません。より賢明な方法は、すべてのサンプルから1つの大きなテキストチャンクを取得し、すべての入力-出力ペアを生成してシャッフルし、均等なサイズのミニバッチを生成することです。\n", "\n", "* **多層LSTM** \n", " LSTMセルを2層または3層試してみるのは理にかなっています。前のユニットで述べたように、LSTMの各層はテキストから特定のパターンを抽出します。文字レベルの生成器の場合、低いLSTM層が音節の抽出を担当し、より高い層が単語や単語の組み合わせを担当すると予想されます。これは、LSTMコンストラクタに層数のパラメータを渡すことで簡単に実装できます。\n", "\n", "* **GRUユニット**を試してみて、どちらがより良い結果を出すかを確認することもできます。また、**異なる隠れ層のサイズ**を試してみるのも良いでしょう。隠れ層が大きすぎると過学習を引き起こす可能性があります(例えば、ネットワークがテキストをそのまま学習してしまう)。一方で、サイズが小さすぎると良い結果が得られない可能性があります。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ソフトテキスト生成と温度\n", "\n", "以前の`generate`の定義では、生成されるテキストの次の文字として、常に最も高い確率を持つ文字を選んでいました。この結果、以下の例のように、テキストが同じ文字列の繰り返しになりがちでした。\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "しかし、次の文字の確率分布を見てみると、いくつかの最も高い確率の間に大きな差がない場合があります。例えば、ある文字の確率が0.2で、別の文字が0.19であるといった具合です。たとえば、'*play*'というシーケンスの次の文字を探す場合、次の文字はスペースでも**e**(単語*player*のように)でも同じくらい適切である可能性があります。\n", "\n", "このことから、常に確率が最も高い文字を選ぶのが「公平」とは限らないという結論に至ります。2番目に高い確率の文字を選んでも、意味のあるテキストにつながる可能性があるのです。ネットワークの出力による確率分布から文字を**サンプリング**する方が賢明です。\n", "\n", "このサンプリングは、いわゆる**多項分布**を実装する`multinomial`関数を使用して行うことができます。この**ソフト**なテキスト生成を実装する関数は以下のように定義されます。\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- Temperature = 0.3\n", "Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n", "\n", "--- Temperature = 0.8\n", "Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n", "\n", "--- Temperature = 1.0\n", "Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n", "\n", "--- Temperature = 1.3\n", "Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n", "\n", "--- Temperature = 1.8\n", "Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n", "\n" ] } ], "source": [ "def generate_soft(net,size=100,start='today ',temperature=1.0):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " #nc = torch.argmax(out[0][-1])\n", " out_dist = out[0][-1].div(temperature).exp()\n", " nc = torch.multinomial(out_dist,1)[0]\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)\n", " \n", "for i in [0.3,0.8,1.0,1.3,1.8]:\n", " print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "私たちは、**温度**と呼ばれるもう1つのパラメーターを導入しました。これは、最高確率にどれだけ厳密に従うべきかを示すために使用されます。温度が1.0の場合、公平な多項分布サンプリングを行い、温度が無限大に近づくと、すべての確率が等しくなり、次の文字をランダムに選択します。以下の例では、温度を上げすぎるとテキストが無意味になることが観察でき、温度が0に近づくと「循環的な」厳密生成されたテキストに似てくることがわかります。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**免責事項**: \nこの文書は、AI翻訳サービス [Co-op Translator](https://github.com/Azure/co-op-translator) を使用して翻訳されています。正確性を追求しておりますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知ください。元の言語で記載された文書が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。この翻訳の使用に起因する誤解や誤解釈について、当方は一切の責任を負いません。\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "coopTranslator": { "original_hash": "7673cd150d96c74c6d6011460094efb4", "translation_date": "2025-08-30T10:12:33+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "ja" } }, "nbformat": 4, "nbformat_minor": 4 }