']
segments += [1] * (len(tokens_b) + 1)
return tokens, segments
BERT は双方向アーキテクチャとして Transformer エンコーダを採用する。
Transformer エンコーダで一般的なように、位置埋め込みは BERT
入力系列の各位置に加えられる。 ただし、元の Transformer
エンコーダとは異なり、BERT では *学習可能* な位置埋め込みを用いる。
要するに、 :numref:`fig_bert-input` が示すように、BERT
入力系列の埋め込みは
トークン埋め込み、セグメント埋め込み、位置埋め込みの和である。
.. _fig_bert-input:
.. figure:: ../img/bert-input.svg
BERT 入力系列の埋め込みは、
トークン埋め込み、セグメント埋め込み、位置埋め込みの和である。
以下の ``BERTEncoder`` クラス は、 :numref:`sec_transformer`
で実装した ``TransformerEncoder`` クラスに似ている。
``TransformerEncoder`` とは異なり、\ ``BERTEncoder`` は
セグメント埋め込みと学習可能な位置埋め込みを用いる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class BERTEncoder(nn.Module):
"""BERT encoder."""
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_blks, dropout, max_len=1000, **kwargs):
super(BERTEncoder, self).__init__(**kwargs)
self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
self.segment_embedding = nn.Embedding(2, num_hiddens)
self.blks = nn.Sequential()
for i in range(num_blks):
self.blks.add_module(f"{i}", d2l.TransformerEncoderBlock(
num_hiddens, ffn_num_hiddens, num_heads, dropout, True))
# In BERT, positional embeddings are learnable, thus we create a
# parameter of positional embeddings that are long enough
self.pos_embedding = nn.Parameter(torch.randn(1, max_len,
num_hiddens))
def forward(self, tokens, segments, valid_lens):
# Shape of `X` remains unchanged in the following code snippet:
# (batch size, max sequence length, `num_hiddens`)
X = self.token_embedding(tokens) + self.segment_embedding(segments)
X = X + self.pos_embedding[:, :X.shape[1], :]
for blk in self.blks:
X = blk(X, valid_lens)
return X
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class BERTEncoder(nn.Block):
"""BERT encoder."""
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_blks, dropout, max_len=1000, **kwargs):
super(BERTEncoder, self).__init__(**kwargs)
self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
self.segment_embedding = nn.Embedding(2, num_hiddens)
self.blks = nn.Sequential()
for _ in range(num_blks):
self.blks.add(d2l.TransformerEncoderBlock(
num_hiddens, ffn_num_hiddens, num_heads, dropout, True))
# In BERT, positional embeddings are learnable, thus we create a
# parameter of positional embeddings that are long enough
self.pos_embedding = self.params.get('pos_embedding',
shape=(1, max_len, num_hiddens))
def forward(self, tokens, segments, valid_lens):
# Shape of `X` remains unchanged in the following code snippet:
# (batch size, max sequence length, `num_hiddens`)
X = self.token_embedding(tokens) + self.segment_embedding(segments)
X = X + self.pos_embedding.data(ctx=X.ctx)[:, :X.shape[1], :]
for blk in self.blks:
X = blk(X, valid_lens)
return X
.. raw:: html
.. raw:: html
語彙サイズが 10000 だと仮定しよう。 ``BERTEncoder`` の順伝播による推論
を示すために、そのインスタンスを作成し、パラメータを初期化する。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
ffn_num_input, num_blks, dropout = 768, 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_blks, dropout)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
num_blks, dropout = 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_blks, dropout)
encoder.initialize()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
[07:06:23] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
.. raw:: html
.. raw:: html
``tokens`` を長さ 8 の 2つの BERT
入力系列と定義し、各トークンは語彙のインデックスであるとする。 入力
``tokens`` に対する ``BERTEncoder``
の順伝播推論は、各トークンがベクトルで表現された符号化結果を返す。
そのベクトル長はハイパーパラメータ ``num_hiddens`` によって定義される。
このハイパーパラメータは通常、Transformer エンコーダの
*隠れサイズ*\ (隠れユニット数)と呼ばれる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
tokens = torch.randint(0, vocab_size, (2, 8))
segments = torch.tensor([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]])
encoded_X = encoder(tokens, segments, None)
encoded_X.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
torch.Size([2, 8, 768])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
tokens = np.random.randint(0, vocab_size, (2, 8))
segments = np.array([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]])
encoded_X = encoder(tokens, segments, None)
encoded_X.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
(2, 8, 768)
.. raw:: html
.. raw:: html
.. _subsec_bert_pretraining_tasks:
事前学習タスク
--------------
``BERTEncoder`` の順伝播推論は、入力テキストの各トークンと挿入された
特別トークン “” および “” の BERT 表現を与える。
次に、これらの表現を用いて BERT の事前学習の損失関数を計算する。
事前学習は次の2つのタスクから構成される:マスク付き言語モデルと次文予測である。
.. _subsec_mlm:
マスク付き言語モデル
~~~~~~~~~~~~~~~~~~~~
:numref:`sec_language-model`
で示したように、言語モデルは左側の文脈を用いてトークンを予測する。
各トークンの表現のために双方向の文脈を符号化するには、BERT
はトークンをランダムにマスクし、双方向文脈からのトークンを用いて自己教師ありでマスクされたトークンを予測する。
このタスクは *マスク付き言語モデル* と呼ばれる。
この事前学習タスクでは、
トークンの15%がランダムに選ばれ、予測対象のマスクされたトークンとなる。
ラベルを使って答えを覗き見しないように、マスクされたトークンを予測するための単純な方法は、BERT
入力系列中でそれを常に特別な “” トークンに置き換えることである。
しかし、この人工的な特別トークン “” は微調整では決して現れない。
事前学習と微調整の間のこのような不一致を避けるため、あるトークンが予測のためにマスクされる場合(たとえば
“this movie is great” で “great”
がマスクおよび予測対象として選ばれた場合)、入力では次のように置き換えられる。
- 80% の確率で特別な “” トークンに置き換える(例:“this movie is
great” が “this movie is ” になる);
- 10% の確率でランダムなトークンに置き換える(例:“this movie is great”
が “this movie is drink” になる);
- 10% の確率でラベルのトークンをそのまま残す(例:“this movie is great”
が “this movie is great” のままになる)。
15% のうち 10% の確率でランダムなトークンが挿入されることに注意しよう。
このときどき入るノイズは、BERT
が双方向文脈の符号化においてマスクされたトークンに過度に偏らないよう促す(特にラベルのトークンがそのまま残る場合)。
以下の ``MaskLM`` クラスを実装して、BERT
事前学習のマスク付き言語モデルタスクにおけるマスクされたトークンを予測する。
予測には1隠れ層 MLP(\ ``self.mlp``\ )を用いる。
順伝播推論では、2つの入力を受け取る: ``BERTEncoder``
の符号化結果と、予測対象のトークン位置である。
出力は、これらの位置における予測結果である。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class MaskLM(nn.Module):
"""The masked language model task of BERT."""
def __init__(self, vocab_size, num_hiddens, **kwargs):
super(MaskLM, self).__init__(**kwargs)
self.mlp = nn.Sequential(nn.LazyLinear(num_hiddens),
nn.ReLU(),
nn.LayerNorm(num_hiddens),
nn.LazyLinear(vocab_size))
def forward(self, X, pred_positions):
num_pred_positions = pred_positions.shape[1]
pred_positions = pred_positions.reshape(-1)
batch_size = X.shape[0]
batch_idx = torch.arange(0, batch_size)
# Suppose that `batch_size` = 2, `num_pred_positions` = 3, then
# `batch_idx` is `torch.tensor([0, 0, 0, 1, 1, 1])`
batch_idx = torch.repeat_interleave(batch_idx, num_pred_positions)
masked_X = X[batch_idx, pred_positions]
masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
mlm_Y_hat = self.mlp(masked_X)
return mlm_Y_hat
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class MaskLM(nn.Block):
"""The masked language model task of BERT."""
def __init__(self, vocab_size, num_hiddens, **kwargs):
super(MaskLM, self).__init__(**kwargs)
self.mlp = nn.Sequential()
self.mlp.add(
nn.Dense(num_hiddens, flatten=False, activation='relu'))
self.mlp.add(nn.LayerNorm())
self.mlp.add(nn.Dense(vocab_size, flatten=False))
def forward(self, X, pred_positions):
num_pred_positions = pred_positions.shape[1]
pred_positions = pred_positions.reshape(-1)
batch_size = X.shape[0]
batch_idx = np.arange(0, batch_size)
# Suppose that `batch_size` = 2, `num_pred_positions` = 3, then
# `batch_idx` is `np.array([0, 0, 0, 1, 1, 1])`
batch_idx = np.repeat(batch_idx, num_pred_positions)
masked_X = X[batch_idx, pred_positions]
masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
mlm_Y_hat = self.mlp(masked_X)
return mlm_Y_hat
.. raw:: html
.. raw:: html
``MaskLM`` の順伝播推論 を示すために、そのインスタンス ``mlm``
を作成して初期化する。 ``BERTEncoder`` の順伝播推論から得られる
``encoded_X`` は、2つの BERT 入力系列を表していることを思い出そう。
``mlm_positions`` を、\ ``encoded_X`` のいずれかの BERT
入力系列において予測する3つのインデックスとして定義する。 ``mlm``
の順伝播推論は、\ ``encoded_X`` のすべてのマスク位置 ``mlm_positions``
における予測結果 ``mlm_Y_hat`` を返す。
各予測について、結果のサイズは語彙サイズに等しい。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
mlm = MaskLM(vocab_size, num_hiddens)
mlm_positions = torch.tensor([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
torch.Size([2, 3, 10000])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
mlm = MaskLM(vocab_size, num_hiddens)
mlm.initialize()
mlm_positions = np.array([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
(2, 3, 10000)
.. raw:: html
.. raw:: html
マスク下で予測されたトークン ``mlm_Y_hat`` の正解ラベル ``mlm_Y``
があれば、BERT
事前学習におけるマスク付き言語モデルタスクの交差エントロピー損失を計算できる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
mlm_Y = torch.tensor([[7, 8, 9], [10, 20, 30]])
loss = nn.CrossEntropyLoss(reduction='none')
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1))
mlm_l.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
torch.Size([6])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
mlm_Y = np.array([[7, 8, 9], [10, 20, 30]])
loss = gluon.loss.SoftmaxCrossEntropyLoss()
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1))
mlm_l.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
(6,)
.. raw:: html
.. raw:: html
.. _subsec_nsp:
次文予測
~~~~~~~~
マスク付き言語モデルは単語の表現のために双方向文脈を符号化できるが、テキスト対の論理的関係を明示的にはモデル化しない。
2つのテキスト系列の関係を理解する助けとして、BERT は事前学習において
*次文予測* という二値分類タスクを考える。
事前学習用の文対を生成する際、半分の確率では実際に連続する文であり、ラベルは
“True” である。
残りの半分では、第2文はコーパスからランダムにサンプリングされ、ラベルは
“False” である。
以下の ``NextSentencePred`` クラスは、1隠れ層 MLP を用いて、BERT
入力系列における第2文が第1文の次の文であるかどうかを予測する。
Transformer エンコーダの自己注意により、特別トークン “” の BERT
表現は入力中の2つの文の両方を符号化する。 したがって、MLP
分類器の出力層(\ ``self.output``\ )は ``X``
を入力として受け取る。ここで ``X`` は、符号化された “”
トークンを入力とする MLP の隠れ層の出力である。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class NextSentencePred(nn.Module):
"""The next sentence prediction task of BERT."""
def __init__(self, **kwargs):
super(NextSentencePred, self).__init__(**kwargs)
self.output = nn.LazyLinear(2)
def forward(self, X):
# `X` shape: (batch size, `num_hiddens`)
return self.output(X)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class NextSentencePred(nn.Block):
"""The next sentence prediction task of BERT."""
def __init__(self, **kwargs):
super(NextSentencePred, self).__init__(**kwargs)
self.output = nn.Dense(2)
def forward(self, X):
# `X` shape: (batch size, `num_hiddens`)
return self.output(X)
.. raw:: html
.. raw:: html
``NextSentencePred`` のインスタンスの順伝播推論 は、各 BERT
入力系列に対して二値予測を返すことがわかる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# PyTorch by default will not flatten the tensor as seen in mxnet where, if
# flatten=True, all but the first axis of input data are collapsed together
encoded_X = torch.flatten(encoded_X, start_dim=1)
# input_shape for NSP: (batch size, `num_hiddens`)
nsp = NextSentencePred()
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
torch.Size([2, 2])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
nsp = NextSentencePred()
nsp.initialize()
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
(2, 2)
.. raw:: html
.. raw:: html
2つの二値分類の交差エントロピー損失も計算できる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
nsp_y = torch.tensor([0, 1])
nsp_l = loss(nsp_Y_hat, nsp_y)
nsp_l.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
torch.Size([2])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
nsp_y = np.array([0, 1])
nsp_l = loss(nsp_Y_hat, nsp_y)
nsp_l.shape
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
(2,)
.. raw:: html
.. raw:: html
前述の2つの事前学習タスクにおけるすべてのラベルは、手作業のラベル付けを行わずとも、事前学習コーパスから自明に得られることに注目すべきである。
元の BERT は BookCorpus :cite:`Zhu.Kiros.Zemel.ea.2015` と英語版
Wikipedia を連結したものを用いて事前学習されている。
これら2つのテキストコーパスは非常に大きく、それぞれ8億語と25億語を含む。
まとめてみよう
--------------
BERT
を事前学習するとき、最終的な損失関数はマスク付き言語モデルと次文予測の両方の損失関数の線形結合である。
ここで、\ ``BERTEncoder``\ 、\ ``MaskLM``\ 、\ ``NextSentencePred``
の3つのクラスをインスタンス化することで、\ ``BERTModel``
クラスを定義できる。 順伝播推論は、符号化された BERT 表現
``encoded_X``\ 、マスク付き言語モデルの予測
``mlm_Y_hat``\ 、および次文予測 ``nsp_Y_hat`` を返す。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class BERTModel(nn.Module):
"""The BERT model."""
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens,
num_heads, num_blks, dropout, max_len=1000):
super(BERTModel, self).__init__()
self.encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens,
num_heads, num_blks, dropout,
max_len=max_len)
self.hidden = nn.Sequential(nn.LazyLinear(num_hiddens),
nn.Tanh())
self.mlm = MaskLM(vocab_size, num_hiddens)
self.nsp = NextSentencePred()
def forward(self, tokens, segments, valid_lens=None, pred_positions=None):
encoded_X = self.encoder(tokens, segments, valid_lens)
if pred_positions is not None:
mlm_Y_hat = self.mlm(encoded_X, pred_positions)
else:
mlm_Y_hat = None
# The hidden layer of the MLP classifier for next sentence prediction.
# 0 is the index of the '' token
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
#@save
class BERTModel(nn.Block):
"""The BERT model."""
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_blks, dropout, max_len=1000):
super(BERTModel, self).__init__()
self.encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens,
num_heads, num_blks, dropout, max_len)
self.hidden = nn.Dense(num_hiddens, activation='tanh')
self.mlm = MaskLM(vocab_size, num_hiddens)
self.nsp = NextSentencePred()
def forward(self, tokens, segments, valid_lens=None, pred_positions=None):
encoded_X = self.encoder(tokens, segments, valid_lens)
if pred_positions is not None:
mlm_Y_hat = self.mlm(encoded_X, pred_positions)
else:
mlm_Y_hat = None
# The hidden layer of the MLP classifier for next sentence prediction.
# 0 is the index of the '' token
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat
.. raw:: html
.. raw:: html
まとめ
------
- word2vec や GloVe
のような単語埋め込みモデルは文脈非依存である。これらは、単語の文脈に関係なく(文脈がある場合でも)同じ単語に同じ事前学習済みベクトルを割り当てる。多義性や自然言語の複雑な意味論をうまく扱うのは難しい。
- ELMo や GPT
のような文脈依存単語表現では、単語の表現はその文脈に依存する。
- ELMo
は文脈を双方向に符号化するが、タスク特化のアーキテクチャを用いる(ただし、自然言語処理タスクごとに個別のアーキテクチャを作るのは実際には容易ではない)。一方、GPT
はタスク非依存だが、文脈を左から右へ符号化する。
- BERT
は両者の長所を組み合わせる。すなわち、文脈を双方向に符号化し、幅広い自然言語処理タスクに対して最小限のアーキテクチャ変更しか必要としない。
- BERT
入力系列の埋め込みは、トークン埋め込み、セグメント埋め込み、位置埋め込みの和である。
- BERT
の事前学習は2つのタスクから成る:マスク付き言語モデルと次文予測である。前者は単語の表現のために双方向文脈を符号化でき、後者はテキスト対の論理的関係を明示的にモデル化する。
演習
----
1. 他の条件がすべて同じなら、マスク付き言語モデルは左から右への言語モデルよりも、収束に必要な事前学習ステップ数が多くなるだろうか、それとも少なくなるだろうか。なぜか。
2. BERT
の元の実装では、\ ``BERTEncoder``\ (\ ``d2l.TransformerEncoderBlock``
経由)の位置ごとのフィードフォワードネットワークと ``MaskLM``
の全結合層の両方で、活性化関数としてガウス誤差線形ユニット(GELU)
:cite:`Hendrycks.Gimpel.2016` が使われている。GELU と ReLU
の違いについて調べよ。