{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# テキスト分類タスク\n", "\n", "このモジュールでは、**[AG_NEWS](http://www.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)** データセットに基づいたシンプルなテキスト分類タスクから始めます。ニュースの見出しを「World(世界)」、「Sports(スポーツ)」、「Business(ビジネス)」、「Sci/Tech(科学/技術)」の4つのカテゴリのいずれかに分類します。\n", "\n", "## データセット\n", "\n", "データセットを読み込むために、**[TensorFlow Datasets](https://www.tensorflow.org/datasets)** API を使用します。\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import tensorflow as tf\n", "from tensorflow import keras\n", "import tensorflow_datasets as tfds\n", "\n", "# In this tutorial, we will be training a lot of models. In order to use GPU memory cautiously,\n", "# we will set tensorflow option to grow GPU memory allocation when required.\n", "physical_devices = tf.config.list_physical_devices('GPU') \n", "if len(physical_devices)>0:\n", " tf.config.experimental.set_memory_growth(physical_devices[0], True)\n", "\n", "dataset = tfds.load('ag_news_subset')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "データセットのトレーニング部分とテスト部分には、それぞれ `dataset['train']` と `dataset['test']` を使用してアクセスできます。\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Length of train dataset = 120000\n", "Length of test dataset = 7600\n" ] } ], "source": [ "ds_train = dataset['train']\n", "ds_test = dataset['test']\n", "\n", "print(f\"Length of train dataset = {len(ds_train)}\")\n", "print(f\"Length of test dataset = {len(ds_test)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "私たちのデータセットから最初の10件の新しい見出しを印刷しましょう。\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3 (Sci/Tech) -> b'AMD Debuts Dual-Core Opteron Processor' b'AMD #39;s new dual-core Opteron chip is designed mainly for corporate computing applications, including databases, Web services, and financial transactions.'\n", "1 (Sports) -> b\"Wood's Suspension Upheld (Reuters)\" b'Reuters - Major League Baseball\\\\Monday announced a decision on the appeal filed by Chicago Cubs\\\\pitcher Kerry Wood regarding a suspension stemming from an\\\\incident earlier this season.'\n", "2 (Business) -> b'Bush reform may have blue states seeing red' b'President Bush #39;s quot;revenue-neutral quot; tax reform needs losers to balance its winners, and people claiming the federal deduction for state and local taxes may be in administration planners #39; sights, news reports say.'\n", "3 (Sci/Tech) -> b\"'Halt science decline in schools'\" b'Britain will run out of leading scientists unless science education is improved, says Professor Colin Pillinger.'\n", "1 (Sports) -> b'Gerrard leaves practice' b'London, England (Sports Network) - England midfielder Steven Gerrard injured his groin late in Thursday #39;s training session, but is hopeful he will be ready for Saturday #39;s World Cup qualifier against Austria.'\n" ] } ], "source": [ "classes = ['World', 'Sports', 'Business', 'Sci/Tech']\n", "\n", "for i,x in zip(range(5),ds_train):\n", " print(f\"{x['label']} ({classes[x['label']]}) -> {x['title']} {x['description']}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## テキストのベクトル化\n", "\n", "次に、テキストを**数値**に変換し、テンソルとして表現できるようにする必要があります。単語レベルでの表現を行いたい場合、以下の2つのステップが必要です:\n", "\n", "* **トークナイザー**を使用してテキストを**トークン**に分割する。\n", "* それらのトークンの**語彙**を構築する。\n", "\n", "### 語彙サイズの制限\n", "\n", "AG Newsデータセットの例では、語彙サイズが非常に大きく、10万語以上あります。一般的に言えば、テキストにほとんど出現しない単語は必要ありません — それらは数文にしか現れず、モデルがそれらから学習することはありません。そのため、ベクトライザーのコンストラクタに引数を渡すことで、語彙サイズを小さな数に制限するのが理にかなっています。\n", "\n", "これらのステップはどちらも**TextVectorization**レイヤーを使用して処理できます。ベクトライザーオブジェクトをインスタンス化し、その後`adapt`メソッドを呼び出してすべてのテキストを処理し、語彙を構築しましょう:\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "vocab_size = 50000\n", "vectorizer = keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size)\n", "vectorizer.adapt(ds_train.take(500).map(lambda x: x['title']+' '+x['description']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **注意** 私たちは、全データセットの一部のみを使用して語彙を構築しています。これは、実行時間を短縮し、待ち時間を減らすためです。しかし、全データセットの中のいくつかの単語が語彙に含まれず、トレーニング中に無視されるリスクを伴います。そのため、全語彙サイズを使用し、`adapt`中に全データセットを処理することで最終的な精度を向上させることは可能ですが、その効果は大きくはありません。\n", "\n", "これで実際の語彙にアクセスできるようになりました:\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['', '[UNK]', 'the', 'to', 'a', 'in', 'of', 'and', 'on', 'for']\n", "Length of vocabulary: 5335\n" ] } ], "source": [ "vocab = vectorizer.get_vocabulary()\n", "vocab_size = len(vocab)\n", "print(vocab[:10])\n", "print(f\"Length of vocabulary: {vocab_size}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ベクトライザーを使用すると、任意のテキストを簡単に数値のセットにエンコードできます。\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vectorizer('I love to play with my words')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bag-of-wordsテキスト表現\n", "\n", "言葉は意味を表すため、時には文中の順序に関係なく、個々の単語を見るだけでテキストの意味を推測できることがあります。例えば、ニュースを分類する際に、*weather*や*snow*といった単語は*天気予報*を示し、*stocks*や*dollar*といった単語は*金融ニュース*に関連する可能性があります。\n", "\n", "**Bag-of-words** (BoW)ベクトル表現は、最も理解しやすい伝統的なベクトル表現です。各単語がベクトルのインデックスにリンクされ、ベクトルの要素には、特定の文書内で各単語が出現した回数が含まれます。\n", "\n", "![Bag-of-wordsベクトル表現がメモリ内でどのように表現されるかを示す画像。](../../../../../translated_images/ja/bag-of-words-example.606fc1738f1d7ba9.webp) \n", "\n", "> **Note**: BoWは、テキスト内の個々の単語に対するすべてのone-hotエンコードされたベクトルの合計として考えることもできます。\n", "\n", "以下は、Scikit LearnのPythonライブラリを使用してBag-of-words表現を生成する例です:\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import CountVectorizer\n", "sc_vectorizer = CountVectorizer()\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "sc_vectorizer.fit_transform(corpus)\n", "sc_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "上記で定義したKerasベクトライザーを使用して、各単語番号をワンホットエンコーディングに変換し、それらのベクトルをすべて加算することもできます。\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0., 5., 0., ..., 0., 0., 0.], dtype=float32)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def to_bow(text):\n", " return tf.reduce_sum(tf.one_hot(vectorizer(text),vocab_size),axis=0)\n", "\n", "to_bow('My dog likes hot dogs on a hot day.').numpy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **注記**: Kerasの例と結果が異なることに驚くかもしれません。その理由は、Kerasの例ではベクトルの長さが語彙サイズに対応しており、この語彙はAG Newsデータセット全体から構築されています。一方、Scikit Learnの例では、サンプルテキストからその場で語彙を構築しているためです。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BoW分類器のトレーニング\n", "\n", "テキストのバッグオブワーズ表現の作り方を学んだので、それを使う分類器をトレーニングしてみましょう。まず、データセットをバッグオブワーズ表現に変換する必要があります。これは、次のように`map`関数を使うことで実現できます:\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "batch_size = 128\n", "\n", "ds_train_bow = ds_train.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)\n", "ds_test_bow = ds_test.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "では、1つの線形層を含むシンプルな分類器ニューラルネットワークを定義しましょう。入力サイズは`vocab_size`で、出力サイズはクラス数(4)に対応します。分類タスクを解くため、最終的な活性化関数は**softmax**です。\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "938/938 [==============================] - 66s 70ms/step - loss: 0.6144 - acc: 0.8427 - val_loss: 0.4416 - val_acc: 0.8697\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model = keras.models.Sequential([\n", " keras.layers.Dense(4,activation='softmax',input_shape=(vocab_size,))\n", "])\n", "model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n", "model.fit(ds_train_bow,validation_data=ds_test_bow)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "4つのクラスがあるため、80%以上の精度は良い結果と言えます。\n", "\n", "## 1つのネットワークとして分類器を訓練する\n", "\n", "ベクトライザもKerasのレイヤーであるため、それを含むネットワークを定義し、エンドツーエンドで訓練することができます。この方法では、`map`を使ってデータセットをベクトル化する必要がなく、元のデータセットをそのままネットワークの入力に渡すことができます。\n", "\n", "> **Note**: データセット内のフィールド(例えば`title`、`description`、`label`など)を辞書からタプルに変換するために、データセットに対して`map`を適用する必要は依然としてあります。ただし、ディスクからデータを読み込む際に、最初から必要な構造を持つデータセットを構築することが可能です。\n" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Model: \"model\"\n", "_________________________________________________________________\n", " Layer (type) Output Shape Param # \n", "=================================================================\n", " input_1 (InputLayer) [(None, 1)] 0 \n", " \n", " text_vectorization (TextVec (None, None) 0 \n", " torization) \n", " \n", " tf.one_hot (TFOpLambda) (None, None, 5335) 0 \n", " \n", " tf.math.reduce_sum (TFOpLam (None, 5335) 0 \n", " bda) \n", " \n", " dense_2 (Dense) (None, 4) 21344 \n", " \n", "=================================================================\n", "Total params: 21,344\n", "Trainable params: 21,344\n", "Non-trainable params: 0\n", "_________________________________________________________________\n", "938/938 [==============================] - 73s 77ms/step - loss: 0.6057 - acc: 0.8414 - val_loss: 0.4202 - val_acc: 0.8736\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def extract_text(x):\n", " return x['title']+' '+x['description']\n", "\n", "def tupelize(x):\n", " return (extract_text(x),x['label'])\n", "\n", "inp = keras.Input(shape=(1,),dtype=tf.string)\n", "x = vectorizer(inp)\n", "x = tf.reduce_sum(tf.one_hot(x,vocab_size),axis=1)\n", "out = keras.layers.Dense(4,activation='softmax')(x)\n", "model = keras.models.Model(inp,out)\n", "model.summary()\n", "\n", "model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n", "model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## バイグラム、トライグラム、n-グラム\n", "\n", "Bag-of-wordsアプローチの制約の一つは、いくつかの単語が複数の単語からなる表現の一部である場合があることです。例えば、「hot dog」という単語は、他の文脈で使われる「hot」や「dog」という単語とは全く異なる意味を持ちます。「hot」と「dog」を常に同じベクトルで表現すると、モデルが混乱する可能性があります。\n", "\n", "これに対処するために、**n-グラム表現**が文書分類の手法でよく使用されます。ここでは、各単語、2語の組み合わせ(バイグラム)、または3語の組み合わせ(トライグラム)の頻度が、分類器を訓練するための有用な特徴となります。例えば、バイグラム表現では、元の単語に加えて、すべての単語ペアを語彙に追加します。\n", "\n", "以下は、Scikit Learnを使用してバイグラムのBag-of-Words表現を生成する例です:\n" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary:\n", " {'i': 7, 'like': 11, 'hot': 4, 'dogs': 2, 'i like': 8, 'like hot': 12, 'hot dogs': 5, 'the': 16, 'dog': 0, 'ran': 14, 'fast': 3, 'the dog': 17, 'dog ran': 1, 'ran fast': 15, 'its': 9, 'outside': 13, 'its hot': 10, 'hot outside': 6}\n" ] }, { "data": { "text/plain": [ "array([[1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],\n", " dtype=int64)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\\b\\w+\\b', min_df=1)\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "bigram_vectorizer.fit_transform(corpus)\n", "print(\"Vocabulary:\\n\",bigram_vectorizer.vocabulary_)\n", "bigram_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "n-gramアプローチの主な欠点は、語彙サイズが非常に速く増加し始めることです。実際には、n-gram表現を次のユニットで説明する*埋め込み*のような次元削減技術と組み合わせる必要があります。\n", "\n", "**AG News**データセットでn-gram表現を使用するには、`TextVectorization`コンストラクタに`ngrams`パラメータを渡す必要があります。バイグラム語彙の長さは**非常に大きく**、私たちの場合では130万以上のトークンがあります!したがって、バイグラムトークンを適切な数に制限するのが理にかなっています。\n", "\n", "上記と同じコードを使用して分類器を訓練することもできますが、それは非常にメモリ効率が悪いでしょう。次のユニットでは、埋め込みを使用してバイグラム分類器を訓練します。それまでの間、このノートブックでバイグラム分類器の訓練を試して、より高い精度を得られるかどうか試してみてください。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BoWベクトルの自動計算\n", "\n", "上記の例では、個々の単語のワンホットエンコーディングを合計することで、手動でBoWベクトルを計算しました。しかし、TensorFlowの最新バージョンでは、ベクトライザーのコンストラクタに`output_mode='count'`パラメータを渡すことで、BoWベクトルを自動的に計算することができます。これにより、モデルの定義とトレーニングが大幅に簡単になります。\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training vectorizer\n", "938/938 [==============================] - 7s 7ms/step - loss: 0.5929 - acc: 0.8486 - val_loss: 0.4168 - val_acc: 0.8772\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model = keras.models.Sequential([\n", " keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='count'),\n", " keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n", "])\n", "print(\"Training vectorizer\")\n", "model.layers[0].adapt(ds_train.take(500).map(extract_text))\n", "model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n", "model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 頻度-逆文書頻度 (TF-IDF)\n", "\n", "BoW(Bag of Words)表現では、単語の出現頻度は単語そのものに関係なく同じ方法で重み付けされます。しかし、*a* や *in* のような頻出単語は、専門用語に比べて分類において重要性が低いことは明らかです。ほとんどのNLPタスクでは、ある単語が他の単語よりも重要である場合があります。\n", "\n", "**TF-IDF**は、**頻度-逆文書頻度**を意味します。これはBag of Wordsの変形で、単語が文書内に出現するかどうかを示す二値の0/1ではなく、コーパス内での単語の出現頻度に関連する浮動小数点値を使用します。\n", "\n", "より正式には、文書$j$内の単語$i$の重み$w_{ij}$は以下のように定義されます:\n", "$$\n", "w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n", "$$\n", "ここで、\n", "* $tf_{ij}$ は文書$j$内で単語$i$が出現した回数、つまり以前見たBoW値\n", "* $N$ はコレクション内の文書数\n", "* $df_i$ はコレクション全体で単語$i$を含む文書数\n", "\n", "TF-IDF値$w_{ij}$は、単語が文書内で出現する回数に比例して増加し、その単語を含むコーパス内の文書数によって調整されます。これにより、ある単語が他の単語よりも頻繁に出現する事実を補正します。例えば、単語がコレクション内の*すべての*文書に出現する場合、$df_i=N$となり、$w_{ij}=0$となります。この場合、その単語は完全に無視されます。\n", "\n", "Scikit Learnを使用すれば、簡単にテキストのTF-IDFベクトル化を作成できます:\n" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0.43381609, 0. , 0.43381609, 0. , 0.65985664,\n", " 0.43381609, 0. , 0. , 0. , 0. ,\n", " 0. , 0. , 0. , 0. , 0. ,\n", " 0. ]])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "vectorizer = TfidfVectorizer(ngram_range=(1,2))\n", "vectorizer.fit_transform(corpus)\n", "vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Kerasでは、`TextVectorization`レイヤーに`output_mode='tf-idf'`パラメータを渡すことで、TF-IDF頻度を自動的に計算できます。TF-IDFを使用すると精度が向上するかどうか確認するために、上記で使用したコードを繰り返してみましょう。\n" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training vectorizer\n", "938/938 [==============================] - 12s 12ms/step - loss: 0.4197 - acc: 0.8662 - val_loss: 0.3432 - val_acc: 0.8849\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model = keras.models.Sequential([\n", " keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='tf-idf'),\n", " keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n", "])\n", "print(\"Training vectorizer\")\n", "model.layers[0].adapt(ds_train.take(500).map(extract_text))\n", "model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n", "model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 結論\n", "\n", "TF-IDF表現は異なる単語に頻度の重みを与えることができますが、意味や順序を表現することはできません。有名な言語学者J.R.ファースが1935年に言ったように、「単語の完全な意味は常に文脈的であり、文脈を離れた意味の研究は真剣に受け止められるべきではない。」このコースの後半では、言語モデルを使用してテキストから文脈情報を捉える方法を学びます。\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": "0cb620c6d4b9f7a635928804c26cf22403d89d98d79684e4529119355ee6d5a5" }, "kernel_info": { "name": "conda-env-py37_tensorflow-py" }, "kernelspec": { "display_name": "py37_tensorflow", "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" }, "nteract": { "version": "nteract-front-end@1.0.0" }, "coopTranslator": { "original_hash": "19b43951d55b377a76209c24c1f017e4", "translation_date": "2025-08-30T10:43:21+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationTF.ipynb", "language_code": "ja" } }, "nbformat": 4, "nbformat_minor": 4 }