Ahogrammer

Deep Dive Into NLP, ML and Cloud

文字ベース言語モデルの作り方

最近の言語処理では言語モデルを使って転移学習をしたり、性能向上に役立てたりするようになってきました。言語モデルの1つであるELMoでは、言語モデルから得られる分散表現を他のタスクの入力に使うことで、質問応答や固有表現認識、評価分析といった様々なタスクの性能向上に役立つことを示しました。ELMoについては以下の記事で詳しく紹介されています。

kamujun.hatenablog.com

よくある言語モデルでは単語単位で学習を行うのですが、文字単位で学習することもできます。そのメリットとしては、文字単位の言語モデルは単語と比べてボキャブラリ数が少ないため学習が高速に進むことや未知語が少ない事が挙げられます。

本記事では文字ベースの言語モデルの作り方について紹介しようと思います。言語モデルを作成し学習したあとは学習したモデルを使ってテキストを生成して見るところまでやってみます。この記事を読むと以下の内容が学べます。

  • 文字ベース言語モデルのためのテキストの準備方法
  • KerasにおけるLSTMを使った言語モデルの作成方法
  • 学習したモデルを使ったテキストの生成方法

記事の構成は以下の3部から成っています。

  • データの準備
  • モデルの学習
  • テキストの生成

では、データの準備から進めていきましょう。

本記事のすべてのコードは以下のリポジトリに格納しています。

github.com

データ準備

まずは学習に使うデータの準備から進めていきます。 その前に、データの準備に関わるので、今回使うモデルがどのようなものか簡単に説明しておきましょう。

今回定義するモデルは文字ベースの言語モデルです。なので、その入出力は当然文字になります。

最初に述べたように言語モデルでは入力の系列に対して出力をします。文字ベースの言語モデルの場合、複数の文字を入力し、その次の文字を予測するというモデルを作成します。今回は10文字をモデルに与え、次の1文字を予測するようなモデルを作成します。

なので、データの準備では生のテキストをモデルで学習できる形式に変換します。具体的には入力10文字とそれに対応する出力1文字を生のテキストから作成します。作成したデータはモデルに与えられるように整数に変換します。つまり、以下のようなデータを作っていきます。

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -> [11]

テキストの読み込み

今回使うテキストはja.text8です。ja.text8は日本語wikipediaを切り出して作られたコーパスです。このコーパス分かち書き済みなので、そのまま単語の分散表現を計算するのに使うこともできます。

ちょん 掛け ( ちょん がけ 、 丁 斧 掛け ・ 手斧 掛け と も 表記 ) と は 、 相撲 の 決まり 手 の ひとつ で ある 。 自分 の 右 ( 左 ) 足 の 踵 を 相手 の 右 ( 左 ) 足 の 踵 に 掛け 、 後方 に 捻っ て 倒す 技 。 ...

まずはja.text8をダウンロードして読み込んでみましょう。

はじめに、コーパスのダウンロードと解凍を行います。 wgetを用いてコーパスをダウンロードし、unzipでファイルを解凍しましょう。 ファイルを解凍すると、ja.text8という名前のコーパスが現れます。

$ wget https://s3-ap-northeast-1.amazonaws.com/dev.tech-sketch.jp/chakki/public/ja.text8.zip
$ unzip ja.text8.zip 
$ ls
-rw-r--r--   1 hironsan  staff   4.1K Aug 31 10:02 char_lstm.py
-rw-r--r--   1 hironsan  staff    95M Oct  4  2017 ja.text8
-rw-r--r--   1 hironsan  staff    32M Oct  4  2017 ja.text8.zip

コーパスのダウンロードと解凍が完了したら、コーパスを読み込みます。以下で定義するload_text関数はファイル名を与えるとテキストを読み込んで返してくれる関数です。

def load_text(filename):
    with open(filename, 'r') as f:
        text = f.read()

    return text

関数を定義したら、解凍したファイル名を与えてテキストを読み込みます。

raw_text = load_text('ja.text8')
print(raw_text[:100])

テキストのクリーニング

次に読み込んだテキストを整形します。ここでは2つのことをします。

1つは読み込んだテキストから空白を取り除きます。ja.text8は分かち書き済みなのですが、今回は単語単位ではなく文字単位で学習させるためです。もう一つは、丸括弧で囲まれたテキストを取り除きます。

以下で定義するclean_textはテキストを与えると、空白と丸括弧で囲まれたテキストを取り除いて返してくれます。

import re

def clean_text(raw_text):
    tokens = raw_text.split()
    cleaned_text = ''.join(tokens)
    pattern = re.compile(r'(.+?)')
    cleaned_text = pattern.sub('', cleaned_text)

    return cleaned_text

関数を定義したらraw_textを与えてクリーニングします。

cleaned_text = clean_text(raw_text)
print(cleaned_text[:100])

今回はお試しなので、10000文字程度を使って学習することにします。

cleaned_text = cleaned_text[:10000]

ボキャブラリの作成

次に、ボキャブラリを作成します。ここでのボキャブラリとは、文字と整数の対応付を格納した辞書のことです。ボキャブラリでは一意な文字に対して重複のないように整数を割り当てていきます。そのマッピングを使って、各文字の系列を整数の系列に変換していきます。そうして、文字の系列を整数の系列に変換することでモデルに入出力できるようになります。

以下で定義するclean_vocabularyはテキストを与えると、ボキャブラリを返してくれます。文字と整数の対応付をした辞書だけでなく、整数と文字の対応付をした辞書も返します。後者の辞書は後でテキストを生成するときに使用します。

from collections import Counter

def create_vocabulary(text):
    char2id = {'<PAD>': 0}
    id2char = {0: '<PAD>'}
    freq = Counter(text)
    for char, _ in freq.most_common():
        id = len(char2id)
        char2id[char] = id
        id2char[id] = char

    return char2id, id2char

関数を定義したら、cleaned_textを与えてボキャブラリを作成します。

char2id, id2char = create_vocabulary(cleaned_text)

以下のようにしてボキャブラリのサイズを表示することができます。

vocab_size = len(char2id)
print('Vocabulary size: {}'.format(vocab_size))
# Vocabulary size: 935

データセットの作成

ボキャブラリが作成できたのでデータセットを作成していきましょう。データセットは入力の系列と出力のラベルを用意します。入力の系列は10文字の連続する文字列を、出力の系列は1文字の文字を与えます。この文字を先ほど作成したボキャブラリを使って整数に変換します。

以下で定義するcreate_datasetはテキストとボキャブラリを与えると、入出力のデータを作成してくれます。このデータはボキャブラリを使って整数にエンコード済みです。

import numpy as np

def create_dataset(text, char2id, maxlen=10):
    sequences = []
    for i in range(maxlen, len(text)):
        seq = text[i - maxlen: i + 1]
        encoded = [char2id[char] for char in seq]
        sequences.append(encoded)

    sequences = np.array(sequences)
    X, y = sequences[:, :-1], sequences[:, -1]

    return X, y

関数を定義したら、cleaned_textchar2idを与えてデータセットを作成します。

X, y = create_dataset(cleaned_text, char2id)
print('X shape: {}'.format(X.shape))
print('y shape: {}'.format(y.shape))
# X shape: (9990, 10)
# y shape: (9990,)

モデルの学習

データセットが用意できたのでモデルの学習を行います。最初にモデルを定義し、その次に学習を行います。

今回定義するモデルは非常に単純です。最初にEmbedding層を使って文字を50次元の埋め込み表現に変換します。その埋め込み表現を隠れ層のユニット数が75のLSTMに入力し、最後に全結合層から各文字の確率を出力します。全結合層に活性化関数としてsoftmaxを指定することで、出力が確率分布になるようにしています。

以下で定義するcreate_modelボキャブラリを与えると、モデルを作成してくれます。

from keras.layers import LSTM, Dense, Embedding
from keras.models import Sequential

def create_model(vocab_size, embedding_dim=50, hidden=75):
    model = Sequential()
    model.add(Embedding(input_dim=vocab_size,
                        output_dim=embedding_dim,
                        mask_zero=True))
    model.add(LSTM(hidden))
    model.add(Dense(vocab_size, activation='softmax'))

    return model

関数を定義したら、vocab_sizeを与えてモデルを作成します。

model = create_model(vocab_size)

モデルのサマリーは以下のようになっています。

model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, None, 50)          46750     
_________________________________________________________________
lstm_1 (LSTM)                (None, 75)                37800     
_________________________________________________________________
dense_1 (Dense)              (None, 935)               71060     
=================================================================
Total params: 155,610
Trainable params: 155,610
Non-trainable params: 0

学習はfitメソッドを使って行います。エポック数は100を設定しておきます。

model.compile(loss='sparse_categorical_crossentropy',
              optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=100, verbose=2)

以下のように学習が進んでいきます。

Epoch 1/100
 - 7s - loss: 5.9173 - acc: 0.0361
Epoch 2/100
 - 6s - loss: 5.5304 - acc: 0.0573
Epoch 3/100
 - 6s - loss: 5.3256 - acc: 0.0729
Epoch 4/100
 - 6s - loss: 5.1065 - acc: 0.0872
Epoch 5/100
 - 6s - loss: 4.8921 - acc: 0.1085
Epoch 6/100
 - 6s - loss: 4.6951 - acc: 0.1319
...

おそらく過学習すると思いますが、今回は試しなのでよしとしましょう。

テキストの生成

では最後に、学習した言語モデルを使ってテキストを生成してみましょう。

テキストの生成ではモデルに10文字を与えて次の文字を予測します。その次に、予測した文字を入力に加えて次の文字を予測します。この操作を繰り返してテキストの生成を行います。そのようなプログラムは以下のように書くことができます。

以下で定義するgenerate_textは事前学習したモデルを使ってテキストを生成します。

from keras.preprocessing.sequence import pad_sequences

def generate_text(model, char2id, id2char, seed_text, maxlen=10, iter=20):
    encoded = [char2id[char] for char in seed_text]
    for _ in range(iter):
        x = pad_sequences([encoded], maxlen=maxlen, truncating='pre')
        y = model.predict_classes(x, verbose=0)
        encoded.append(y[0])
    decoded = [id2char[c] for c in encoded]
    text = ''.join(decoded)

    return text

関数を定義したら、シードとなるテキスト「ちょん掛けとは、相撲」を与えてテキストを生成します。

print(generate_text(model, char2id, id2char, 'ちょん掛けとは、相撲'))
# ちょん掛けとは、相撲の決まり手のひとつである。自分の右足の踵

上で与えたテキストは、学習データに含まれているので生成できるのは当然です。 次に、学習データに含まれていないテキストをシードに与えて生成してみましょう。

おわりに

自然言語処理において、言語モデルは応用範囲の広い技術で、ますます重要になってきています。今回は文字ベースの言語モデルの作り方について紹介しました。この記事が皆様のお役に立てば幸いです。

私のTwitterアカウントでも機械学習自然言語処理に関する情報をつぶやいています。 @Hironsan

この分野にご興味のある方のフォローをお待ちしています。

参考資料