こんにちには。@historoidです。
この記事の目次
自然言語処理の基礎

今回は、自然言語処理の基礎について学んでいきたいと思います。
本記事は、Googleの公式ドキュメントを参考にしています。
自然言語とは
自然言語とは、人間が使用している言語です。
対義語は人工語で、プログラミング言語がそうです。
両者の違いは、言語の曖昧さです。プログラミング言語は書いたとおりに動いてほしいので、コードの意味は一意に定まらなければなりません。
一方で、自然言語には曖昧なところがあります。例えば「低い声の大きな男」というような、「低い声」の「大きな男」なのか、「声の大きな」「男」なのかわかりません。
自然言語処理とは
自然言語処理(natural language processing, NLP)とは自然言語をコンピュータに処理させることを目的とした分野あるいはその技術を指します。
単語分散表現
単語分散表現
単語分散表現とは、文章中の単語をベクトル化する手法のことです。
以下にいくつか紹介します。
単語数と語彙数
The cat sat on the mat.
この文章は”the”, “cat”, “sat”, “on”, “mat”という5つの単語からなります。
ここで「文章を構成する単語の数」と「単語の種類数」を区別するため、「単語数」と「語彙数」という数を定義します。
“the”は2回出現するので、文章の単語数は6ですが、語彙数は5です。
one-hot encodeing
では、one-hot encoding(OHE)で”The cat sat on the mat.”を表現してみましょう。
OHEとは、要素N個のベクトルのうち1つだけが数字の1で、残りはゼロのベクトルで表現する方法です。
先ほどの例でいうと、語彙数がN個に相当します。今回は5個です。
$$
the \Rightarrow (0, 0, 0, 0, 1), \\
cat \Rightarrow (1, 0, 0, 0, 0), \\
$$
このようなベクトルとして1つの単語を表すことができます。なお、特にどの要素が1になるかは決まっていません。
OHEの利点と欠点
OHEは、単語を離散化できる点で有利です。
例えばtheを(0, 1)、catを(0, 2)としても良いのですが、「theを2倍するとcatになる」わけではありません。この場合、数値の大きさには意味はありません。
ですが機械学習内部では行列計算が行われるため、数値の大きさが違っていては困ります。その点で、ベクトルは長くなるものの0か1だけで表されるOHEは便利なのです。
しかし、OHEはほとんどの要素が0のベクトルです。素なベクトルは計算上不利に働きます。
単語にユニークな数を割り振る
OHEとは別の方法もあります。
theに1を、catに2を、というように数字を割り振る方法です。
この場合、OHEとは異なり密なベクトルを作ることができますが、数値の大きさに差が出てきてしまいます。
単語埋め込み
上記2つの方法の良い点を採用したのが単語埋め込み(word embedding)です。
$$cat = (0.1, -1.2, 0.3, 3.7)$$
例えばこんなベクトルでcatを表します。要素数は自分で決めてOKです。
今は例なので適当な値を入れていますが、各要素の値を学習によって決定することを単語埋め込みと言います。
単語埋め込みの実装

ではさっそく単語埋め込みを実装してみましょう。
まずはライブラリのインポート
!pip install tf-nightly
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
tf-nightlyは数値計算用のライブラリです。
tfds.disable_progress_bar()でプログレスバーを消しています。
Embeddingレイヤー
では単語埋め込み用のレイヤーを実装しましょう。
embedding_layer = layers.Embedding(1000, 5)
ここでは、語彙数が1000あり、それを5次元のベクトルで表現することを想定しています。
Embeddingレイヤーに入力してみよう
result = embedding_layer(tf.constant([1,2,3]))
result.numpy()
'''
array([[-0.02488925, 0.00560363, 0.02322726, -0.00617164, 0.03427159],
[-0.04101037, 0.04502201, -0.04498826, -0.0119302 , 0.00057654],
[ 0.04166016, -0.04719774, -0.00023725, 0.03618462, -0.00676106]],
dtype=float32)
'''
この例では、Embedding層に整数のリスト[1, 2, 3]を入力しています。
1に対して、[-0.02488925, 0.00560363, 0.02322726, -0.00617164, 0.03427159]というリストが返ってきます。3つ入力したので、3つのリストが返ってきていることが分かります。
最もシンプルに書くなら、embedding_layer(tf.constant(1))ですね。これも同じ結果です。
Embedding層にリストのリストを入力する
[ [0], [1] ]を入力してみます。
embedding_layer(tf.constant([[0],[1]]))
'''
array([[[-0.00867957, -0.00418412, -0.0017279 , -0.04392438,
-0.01310413]],
[[-0.02488925, 0.00560363, 0.02322726, -0.00617164,
0.03427159]]], dtype=float32)
'''
返り値は同じように見えていますが、括弧が増えています。
返り値もリストのリストになっているわけですね。
埋め込みを学習する
これまでの例では、整数を入力して、初期の重みが返ってきていました。
ここでは埋め込みの重みを学習させます。
訓練データセットはIMDBの映画レビューです。
(train_data, test_data), info = tfds.load(
'imdb_reviews/subwords8k',
split = (tfds.Split.TRAIN, tfds.Split.TEST),
with_info=True, as_supervised=True)
このデータはすでに前処理が済んだデータです。
語彙の確認
データに含まれる語彙を確認してみます。
encoder = info.features['text'].encoder
encoder.subwords[:5]
# ['the_', ', ', '. ', 'a_', 'and_']
映画レビューに含まれる全語彙を出現頻度で並べ、はじめの5つを抜き出しました。ピリオドやコンマも含まれているんですね。
train_batches = train_data.shuffle(1000).padded_batch(10)
test_batches = test_data.shuffle(1000).padded_batch(10)
train_batch, train_labels = next(iter(train_batches))
ここではレビューの単語数を1000個に指定しています。1000語以内のレビューもあるでしょうし、1000語以上のレビューもあるでしょう。しかしとりあえず今は、1000個で区切っています。
1000語未満のレビューは0で欠損値が埋められます。
モデルの構築
単純なシーケンシャルモデルを作ってみましょう。
embedding_dim=16
model = keras.Sequential([
layers.Embedding(encoder.vocab_size, embedding_dim),
layers.GlobalAveragePooling1D(),
layers.Dense(16, activation='relu'),
layers.Dense(1)
])
model.summary()
Embedding層には、encoder.vocab_sizeとembedding_dimが与えられます。今回の場合は、約8000個の語彙を16次元で表します。
訓練と可視化
model.compile(optimizer='adam',
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
history = model.fit(
train_batches,
epochs=10,
validation_data=test_batches, validation_steps=20)
訓練に関しては、画像のときと変わらないですね。
import matplotlib.pyplot as plt
history_dict = history.history
acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss=history_dict['loss']
val_loss=history_dict['val_loss']
epochs = range(1, len(acc) + 1)
plt.figure(figsize=(12,9))
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.figure(figsize=(12,9))
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim((0.5,1))
plt.show()
以下が正解率のグラフです。点が訓練データです。

続いて、損失です。

過学習していることが見て取れますが、このように学習は進むわけですね。
訓練結果の保存
訓練結果を保存して、TensorBoardで見てみましょう。
ローカルで実装しているなら、TensorBoardはそのまま見ることができますが、Google Colabだと見れないので、訓練結果を一旦保存します。
e = model.layers[0]
weights = e.get_weights()[0]
print(weights.shape) # shape: (vocab_size, embedding_dim)
import io
encoder = info.features['text'].encoder
out_v = io.open('vecs.tsv', 'w', encoding='utf-8')
out_m = io.open('meta.tsv', 'w', encoding='utf-8')
for num, word in enumerate(encoder.subwords):
vec = weights[num+1] # 0 はパディングのためスキップ
out_m.write(word + "\n")
out_v.write('\t'.join([str(x) for x in vec]) + "\n")
out_v.close()
out_m.close()
# ローカルにダウンロードするなら以下を実行
try:
from google.colab import files
except ImportError:
pass
else:
files.download('vecs.tsv')
files.download('meta.tsv')
次に、http://projector.tensorflow.org/へアクセスしましょう。
さきほど作った2つのファイルをアップロードすると学習されたベクトルを確認することができます。
1つ1つの点が語彙を示しています。
もともとは16次元のベクトルなので、主成分分析で3次元まで落としてあります。これによって、各語彙の距離(つまり類似度)がわかります。
リカレントニューラルネットワーク

ここまでが単語埋め込みの話です。
ざっくり言うと、映画レビューの「好意的」と「否定的」という指標から、レビューを構成する各単語の性質(ベクトル)を予測したわけですね。
そしてそれを可能にするのは、第一にEmbedding層です。単語をベクトルにしないと計算できませんから。
そしてそのあとは、GlobalAveragePooling層とDense層だけで学習させました。
では次は、リカレントニューラルネットワーク(RNN)で学習してみましょう!
リカレント(再帰的)な分析が必要な理由
CNNでは「畳込み」という手法を使いました。画像では隣り合うピクセル同士の関係が大事だからです。
同じように、文章では単語の前後関係(時系列)が重要ですよね。だから、ある単語の前にどんな単語が出てきたのかを考慮する必要があります。
RNNの詳しい説明は省略しますが、RNNには前のニューロンの出力とは別に「隠れ状態」というものを引き受ける機能があります。
ではまあ実装してみましょう。
方法1
RNNの1つであるLSTM層を使ったモデルを紹介します。
model = tf.keras.Sequential([
tf.keras.layers.Embedding(encoder.vocab_size, 16),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(16)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
optimizer=tf.keras.optimizers.Adam(1e-4),
metrics=['accuracy'])
history = model.fit(train_dataset, epochs=10,
validation_data=test_dataset,
validation_steps=30)
可視化します。
import matplotlib.pyplot as plt
def plot_graphs(history, metric):
plt.plot(history.history[metric])
plt.plot(history.history['val_'+metric], '')
plt.xlabel("Epochs")
plt.ylabel(metric)
plt.legend([metric, 'val_'+metric])
plt.show()
plot_graphs(history, 'accuracy')
plot_graphs(history, 'loss')
以下が結果です。


ただのSeqモデルとあまり変わりませんでしたね。
方法2
次はLSTMを2層重ねてみます。
model = tf.keras.Sequential([
tf.keras.layers.Embedding(encoder.vocab_size, 16),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(16)),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(8)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
optimizer=tf.keras.optimizers.Adam(1e-4),
metrics=['accuracy'])
history = model.fit(train_dataset, epochs=10,
validation_data=test_dataset,
validation_steps=30)


この結果は方法1と変わっていないように見えます。
コード上のミスだと思われますが、現在のところ間違いを探せていないので、一応載せておきます。
[st-kaiwa1]
おつかれさまでした。今回はここまでです。
自然言語処理のプログラムの流れをご理解いただけたでしょうか。