9.3. 言語モデル¶
9.2 章 では、テキスト系列をトークンへ写像する方法を見た。ここでこれらのトークンは、単語や文字のような離散的な観測の系列としてみなせる。長さ \(T\) のテキスト系列のトークンを順に \(x_1, x_2, \ldots, x_T\) とする。 言語モデル の目標は、系列全体の同時確率を推定することである。
ここで 9.1 章 の統計的手法を適用できる。
言語モデルは非常に有用である。たとえば、理想的な言語モデルは、1トークンずつ \(x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1)\) とサンプリングするだけで、自ら自然なテキストを生成できるはずである。 タイプライターを打つサルとはまったく異なり、このようなモデルから出てくるテキストはすべて自然言語、たとえば英語として通用するだろう。さらに、過去の対話断片を条件にするだけで、意味のある対話を生成するのにも十分である。 明らかに、私たちはそのようなシステムの設計からはまだほど遠い。なぜなら、それには単に文法的にもっともらしい内容を生成するだけでなく、テキストを 理解 する必要があるからである。
それでも、言語モデルは制約された形であっても大いに役立つ。 たとえば、「to recognize speech」と「to wreck a nice beach」は非常によく似た響きを持つ。 これは音声認識において曖昧性を生みうるが、 言語モデルを用いれば、後者の変換を突飛なものとして棄却することで容易に解決できる。 同様に、文書要約アルゴリズムでは、 「dog bites man」が「man bites dog」よりはるかに頻出であることや、「I want to eat grandma」がかなり不穏な表現である一方、「I want to eat, grandma」ははるかに無害であることを知っていると有益である。
%load_ext d2lbook.tab
tab.interact_select(['mxnet', 'pytorch', 'tensorflow', 'jax'])
from d2l import torch as d2l
import torch
from d2l import mxnet as d2l
from mxnet import np, npx
npx.set_np()
from d2l import jax as d2l
from jax import numpy as jnp
from d2l import tensorflow as d2l
import tensorflow as tf
9.3.1. 言語モデルの学習¶
当然の疑問は、文書、あるいはトークン系列そのものをどのようにモデル化すべきかである。 テキストデータを単語レベルでトークン化するとしよう。 まずは基本的な確率の法則を適用してみる。
たとえば、 4語からなるテキスト系列の確率は次のように表される。
9.3.1.1. マルコフモデルと \(n\)-gram¶
9.1 章 における系列モデルの分析の中で、 言語モデリングにマルコフモデルを適用してみよう。 系列上の分布が1次のマルコフ性 \(P(x_{t+1} \mid x_t, \ldots, x_1) = P(x_{t+1} \mid x_t)\) を満たすとする。高次の場合は、より長い依存関係に対応する。これにより、系列をモデル化するために適用できるいくつかの近似が得られる。
1変数、2変数、3変数を含む確率式は、それぞれ通常 unigram、bigram、trigram モデルと呼ばれる。 言語モデルを計算するには、単語の確率と、直前のいくつかの単語が与えられたときの単語の条件付き確率を計算する必要がある。 注意すべきなのは、 このような確率が 言語モデルのパラメータであるということである。
9.3.1.2. 単語頻度¶
ここでは、 学習データセットが、Wikipedia の全項目や Project Gutenberg、 およびウェブ上に投稿されたすべてのテキストのような、大規模なテキストコーパスであると仮定する。 単語の確率は、学習データセットにおける対象単語の相対頻度から計算できる。 たとえば、推定値 \(\hat{P}(\textrm{deep})\) は、「deep」で始まる任意の文の確率として計算できる。やや精度の低い方法としては、「deep」という単語の出現回数をすべて数え、それをコーパス中の総単語数で割る方法がある。 これはかなりうまく機能し、とくに頻出語では有効である。さらに進めると、次を推定できる。
ここで \(n(x)\) と \(n(x, x')\) は、それぞれ単独の単語と連続する単語対の出現回数である。 残念ながら、 単語対の確率を推定するのはやや難しい。というのも、「deep learning」の出現頻度はずっと低いからである。 とくに、珍しい単語の組み合わせでは、十分な出現回数を見つけて正確な推定を得るのが難しい場合がある。 9.2.5 章 の実証結果が示唆するように、3語以上の組み合わせでは状況はさらに悪化する。 データセット中で見かけないであろう、もっともらしい3語の組み合わせは数多く存在する。こうした単語の組み合わせに非ゼロのカウントを割り当てる解決策を用意しない限り、言語モデルでそれらを使うことはできない。データセットが小さい場合や単語が非常に稀な場合には、そのうち1つも見つからないことさえある。
9.3.1.3. ラプラス平滑化¶
一般的な戦略の1つは、何らかの形の ラプラス平滑化 を行うことである。 解決策は、すべてのカウントに小さな定数を加えることである。 学習集合中の単語総数を \(n\)、 異なる単語数を \(m\) とする。 この解決策は単独語に対して有効であり、たとえば次のように表せる。
ここで \(\epsilon_1,\epsilon_2\), および \(\epsilon_3\) はハイパーパラメータである。 \(\epsilon_1\) を例に取ると、 \(\epsilon_1 = 0\) のとき平滑化は行われない。 \(\epsilon_1\) が正の無限大に近づくと、 \(\hat{P}(x)\) は一様確率 \(1/m\) に近づく。 上記は、他の手法が実現できること (Wood et al., 2011) のかなり原始的な変種である。
残念ながら、このようなモデルは次の理由からすぐに扱いにくくなる。 第一に、 9.2.5 章 で述べたように、 多くの \(n\)-gram は非常に稀にしか現れず、 ラプラス平滑化は言語モデリングにはあまり適していない。 第二に、すべてのカウントを保存する必要がある。 第三に、これは単語の意味を完全に無視している。たとえば、「cat」と「feline」は関連する文脈で現れるべきである。 このようなモデルを追加の文脈に合わせて調整するのはかなり難しいが、深層学習ベースの言語モデルはこれを取り込むのに適している。 最後に、長い単語系列はほぼ確実に新規のものになるため、過去に見た単語系列の頻度を単に数えるだけのモデルはそこでうまく機能しない。 したがって、本章の残りでは、言語モデリングにニューラルネットワークを用いることに焦点を当てる。
9.3.2. 困惑度¶
次に、言語モデルの品質をどのように測るかを議論し、その後の節でモデルを評価するために用いる。 1つの方法は、テキストがどれだけ意外かを調べることである。 良い言語モデルは、次に来るトークンを高い精度で予測できる。 異なる言語モデルが提案する「It is raining」の続きとして、次の例を考えよう。
“It is raining outside”
“It is raining banana tree”
“It is raining piouw;kcj pwepoiut”
品質の観点では、例1が明らかに最良である。単語は意味をなし、論理的にも整合している。 意味的にどの単語が続くかを完全に正確に反映しているわけではないかもしれないが(「in San Francisco」や「in winter」でもまったく妥当な続きである)、モデルはどの種類の単語が続くかを捉えることができている。 例2は、無意味な続きが生成されるため、かなり悪い。それでも少なくとも、モデルは単語の綴り方と、単語間のある程度の相関を学習している。最後に、例3は、データにうまく適合していない、訓練不十分なモデルを示している。
系列の尤度を計算することでモデルの品質を測ることもできる。 残念ながら、これは理解しにくく比較もしづらい数値である。 そもそも、短い系列のほうが長い系列よりはるかに起こりやすいので、 トルストイの大作 War and Peace でモデルを評価すると、たとえばサン=テグジュペリの中編 The Little Prince で評価した場合より、尤度は必然的にずっと小さくなる。欠けているのは平均に相当するものである。
ここで情報理論が役立つ。 ソフトマックス回帰を導入したときに、エントロピー、驚き、クロスエントロピーを定義した (4.1.3 章)。 テキストを圧縮したいなら、 現在のトークン集合が与えられたときに次のトークンを予測することを考えればよい。 より良い言語モデルほど、次のトークンをより正確に予測できる。 したがって、系列を圧縮するのに必要なビット数をより少なくできる。 そこで、系列の \(n\) 個のトークン全体で平均したクロスエントロピー損失として測定できる。
ここで \(P\) は言語モデルによって与えられ、\(x_t\) は時刻 \(t\) に系列から観測された実際のトークンである。 これにより、異なる長さの文書に対する性能を比較可能にできる。歴史的な理由から、自然言語処理の研究者は perplexity と呼ばれる量を用いることを好む。要するに、これは (9.3.7) の指数である。
困惑度は、次にどのトークンを選ぶかを決めるときに実際に取りうる選択肢の数の幾何平均の逆数として理解するのが最もよい。いくつかの場合を見てみよう。
最良の場合、モデルは常に目標トークンの確率を1と完全に推定する。このときモデルの困惑度は1である。
最悪の場合、モデルは常に目標トークンの確率を0と予測する。このとき困惑度は正の無限大である。
基準として、モデルは語彙中の利用可能なすべてのトークンに対して一様分布を予測する。この場合、困惑度は語彙の異なるトークン数に等しい。実際、系列を何の圧縮もせずに保存するなら、これが符号化のためにできる最善である。したがって、これは有用なモデルが必ず上回らなければならない、非自明な上界を与える。
9.3.3. 系列の分割¶
ここでは、ニューラルネットワークを用いて言語モデルを設計し、 困惑度を使って、 テキスト系列において現在のトークン集合が与えられたときに次のトークンを予測する性能がどれほど良いかを評価する。 モデルを導入する前に、 それがあらかじめ定められた長さの系列のミニバッチを一度に処理すると仮定しよう。 ここでの問題は、入力系列と目標系列のミニバッチをランダムに読み込むにはどうすればよいか、である。
データセットが corpus にある \(T\)
個のトークンインデックスからなる系列の形をとるとしよう。 これを
各部分系列が \(n\)
個のトークン(時刻)を持つような部分系列に分割する。
各エポックでデータセット全体の(ほぼ)すべてのトークンを走査し、 長さ
\(n\) のすべての可能な部分系列を得るために、
ランダム性を導入できる。 より具体的には、 各エポックの最初に、 最初の
\(d\) 個のトークンを捨てる。ここで \(d\in [0,n)\)
は一様にランダムサンプリングされる。 その後、残りの系列を
\(m=\lfloor (T-d)/n \rfloor\) 個の部分系列に分割する。 時刻
\(t\) でトークン \(x_t\) から始まる長さ \(n\) の部分系列を
\(\mathbf x_t = [x_t, \ldots, x_{t+n-1}]\) と表す。
すると、分割された \(m\) 個の部分系列は
\(\mathbf x_d, \mathbf x_{d+n}, \ldots, \mathbf x_{d+n(m-1)}.\)
各部分系列は、言語モデルへの入力系列として用いられる。
言語モデリングでは、 これまで見てきたトークンに基づいて次のトークンを予測することが目標である。したがって、目標(ラベル)は元の系列を1トークンずらしたものである。 任意の入力系列 \(\mathbf x_t\) に対する目標系列は、長さ \(n\) の \(\mathbf x_{t+1}\) である。
図 9.3.1 分割された長さ5の部分系列から5組の入力系列と目標系列を得る。¶
図 9.3.1 は、\(n=5\) と \(d=2\) のときに5組の入力系列と目標系列を得る例を示している。
@d2l.add_to_class(d2l.TimeMachine) #@save
def __init__(self, batch_size, num_steps, num_train=10000, num_val=5000):
super(d2l.TimeMachine, self).__init__()
self.save_hyperparameters()
corpus, self.vocab = self.build(self._download())
array = d2l.tensor([corpus[i:i+num_steps+1]
for i in range(len(corpus)-num_steps)])
self.X, self.Y = array[:,:-1], array[:,1:]
言語モデルを訓練するために、 入力系列と目標系列の組を
ミニバッチでランダムにサンプリングする。
以下のデータローダーは、データセットから毎回ランダムにミニバッチを生成する。
引数 batch_size は各ミニバッチ中の部分系列例の数を指定し、
num_steps はトークン単位の部分系列長である。
@d2l.add_to_class(d2l.TimeMachine) #@save
def get_dataloader(self, train):
idx = slice(0, self.num_train) if train else slice(
self.num_train, self.num_train + self.num_val)
return self.get_tensorloader([self.X, self.Y], train, idx)
以下でわかるように、 目標系列のミニバッチは 入力系列を1トークンずらすことで 得られる。
data = d2l.TimeMachine(batch_size=2, num_steps=10)
for X, Y in data.train_dataloader():
print('X:', X, '\nY:', Y)
break
X: tensor([[16, 4, 21, 2, 8, 16, 15, 2, 13, 0],
[ 0, 8, 19, 2, 23, 10, 21, 2, 21, 10]])
Y: tensor([[ 4, 21, 2, 8, 16, 15, 2, 13, 0, 21],
[ 8, 19, 2, 23, 10, 21, 2, 21, 10, 16]])
9.3.4. 要約と考察¶
言語モデルはテキスト系列の同時確率を推定する。長い系列に対しては、\(n\)-gram は依存関係を切り詰めることで便利なモデルを提供する。しかし、構造は多いのに頻度が十分でないため、ラプラス平滑化で稀な単語の組み合わせを効率よく扱うのは難しい。したがって、以降の節ではニューラル言語モデリングに焦点を当てる。 言語モデルを訓練するには、入力系列と目標系列の組をミニバッチでランダムにサンプリングできる。訓練後は、困惑度を用いて言語モデルの品質を測定する。
言語モデルは、データ量、モデルサイズ、訓練計算量を増やすことでスケールアップできる。大規模言語モデルは、入力テキストの指示に基づいて出力テキストを予測することで、望ましいタスクを実行できる。後で議論するように(たとえば 11.9 章)、 現時点では 大規模言語モデルが、多様なタスクにおける最先端システムの基盤を成している。
9.3.5. 演習¶
学習データセットに10万語があると仮定する。4-gram はどれだけの単語頻度と隣接する複数語の頻度を保存する必要があるか。
対話をどのようにモデル化すべきか。
長い系列データを読み込むための他の方法として、どのようなものが考えられるか。
各エポックの最初に、最初の数個のトークンを一様ランダムな数だけ捨てる方法を考えよ。
これは文書上の系列に対して、本当に完全に一様な分布をもたらすだろうか。
さらに一様にするには何をすべきだろうか。
ある系列例を完全な文にしたい場合、ミニバッチサンプリングではどのような問題が生じるだろうか。どうすれば修正できるだろうか。