For Your ISHIO Blog

データ分析や機械学習やスクラムや組織とか、色々つぶやくブログです。

学習済みEmbeddingを利用する時の前処理ゴールデンルール

Word2vecやfastText、Gloveなど、Word Embeddingの方法は広く普及してきており、外部から学習済みのEmbeddingデータをインポートし、そのベクトルを手元のデータセットに適用し利用するケースも増えています。

学習済みEmbeddingを効果的に利用するためには、一般的な自然言語の前処理とは異なるアプローチが必要らしいです。次のKernelでは、ゴールデンルールとして紹介されていますので、このブログで触れたいと思います。

How to: Preprocessing when using embeddings | Kaggle

目次

そもそもEmbeddingとは

こちらをご覧ください。

ishitonton.hatenablog.com

2つのゴールデンルール

次の2点が前処理の方針です。

  1. 学習済みEmbeddingを利用する際には、ステミングやストップワードの除去等の標準的な前処理ステップを利用しない。
  2. 自データセットのvocabularyをできるだけ、Embedding側に近づけるように処理を行っていく。

ここでの注意点としては、利用する学習済みEmbedding毎に、Embedding作成までのプロセス(前処理や除外ルール)は異なる点です。つまり、このEmbeddingに近づけるための作業は画一的な手順ではなく、利用するEmbeddingと対話をしながら、ある程度探索的なデータ分析プロセスを挟むことになります。

利用するデータセット

用意するのは、次の2つです。

  1. 学習済みEmbeddingデータを適用するデータセット
  2. 学習済みEmbeddingデータ

前者には、Kernel同様KaggleのQuora Insincere Questions Classificationコンペのデータセット(trainデータ)を利用します。

Quora Insincere Questions Classification | Kaggle

後者には、GoogleNewsをもとに作成されたWord2vecのEmbeddingを利用します。次のリンクからダウンロード可能です(約2GB)。

GoogleNews-vectors-negative300 | Kaggle

適用先のデータセット

Quoraは実名制のQ&Aサイトで、データセットには質問のテキストデータが含まれています。

text.head()
0    How did Quebec nationalists see their province...
1    Do you have an adopted dog, how would you enco...
2    Why does velocity affect time? Does velocity a...
3    How did Otto von Guericke used the Magdeburg h...
4    Can I convert montra helicon D to a mountain b...
Name: question_text, dtype: object

データセットのVocabularyを作成

テキストデータからVocabularyの辞書を作成していきます。

def build_vocab(sentences, verbose =  True):
    vocab = {}
    for sentence in sentences:
        for word in sentence:
            try:
                vocab[word] += 1
            except KeyError:
                vocab[word] = 1
    return vocab

sentences = text.apply(lambda x: x.split()).values
vocab = build_vocab(sentences)
print(vocab)
{'How': 261930,
 'did': 33489,
 'Quebec': 97,
 'nationalists': 91,
 'see': 9003,
 'their': 34810,
...

学習済みEmbeddingの読み込み

gensimを利用して、Word2Vecの学習済みEmbeddingを読み込みます。

from gensim.models import KeyedVectors

news_path = 'GoogleNews-vectors-negative300.bin'
embeddings_index = KeyedVectors.load_word2vec_format(news_path, binary=True)

試しにjapanという単語をみてみます。

embeddings_index["japan"]
array([-3.22265625e-01,  2.30712891e-02,  1.77734375e-01,  3.35937500e-01,
       -2.75390625e-01, -3.01513672e-02, -2.11914062e-01, -2.98828125e-01,
        3.76953125e-01, -1.13281250e-01, -6.54296875e-02, -3.53515625e-01,
       ...
       -6.95312500e-01,  2.50000000e-01,  7.66601562e-02,  2.02148438e-01],
      dtype=float32)
print(embeddings_index["japan"])
300

このEmbeddingは、300次元からなるベクトル表現であることがわかります。

vocabと外部Embeddingの単語の重複チェック

Out of Vocabulary(OoV)はすなわち知らない単語のことを指します。データセットと外部の学習済みEmbeddingには、存在する単語に差異があります。外部Embedding(Google News)には存在しないが、データセットのVocabularyには存在する単語がOoVです。これを利用することで、前処理を改善することが可能です。

以下は、vocabとembeddingとの間の共通部分およびOoVをチェックする関数です。

import operator 
from tqdm import tqdm

def check_coverage(vocab, embeddings_index):
    a = {}
    oov = {}
    k = 0
    i = 0
    for word in tqdm(vocab):
        try:
            a[word] = embeddings_index[word]
            k += vocab[word]
        except:

            oov[word] = vocab[word]
            i += vocab[word]
            pass

    print('Found embeddings for {:.2%} of vocab'.format(len(a) / len(vocab)))
    print('Found embeddings for  {:.2%} of all text'.format(k / (k + i)))
    sorted_x = sorted(oov.items(), key=operator.itemgetter(1))[::-1]

    return sorted_x

実行します。

oov = check_coverage(vocab, embeddings_index)
Found embeddings for 24.31% of vocab
Found embeddings for  78.75% of all text

出力結果からは、Quoraのテキストデータから得られた単語の24.31%しか、外部Embeddingデータに含まれていないことがわかります。これでは、外部のEmbeddingを利用して、テキストデータに内包される情報を効果的に表現することができません。データクレンジングを通じて、できるだけこの割合を高めていきます。(=OoVを減らしていく。)

OoVの出力し、改善の糸口を探る

OoVを出力してみます。

oov[:20]
[('to', 403183),
 ('a', 402682),
 ('of', 330825),
 ('and', 251973),
 ('India?', 16384),
 ('it?', 12900),
 ('do?', 8753),
 ('life?', 7753),
 ('you?', 6295),
 ('me?', 6202),
 ('them?', 6140),
 ('time?', 5716),
 ('world?', 5386),
 ('people?', 4971),
 ('why?', 4943),
 ('Quora?', 4655),
 ('10', 4591),
 ('like?', 4487),
 ('for?', 4450),
 ('work?', 4206)]

サンプル出力からいくつかのことがわかります。

  • 語尾に?がついたVocabがEmbeddingには存在していない。?とか記号分割の処理をいい感じにすればOoVを減らせる。
  • atoofandGoogle newsのEmbeddingには存在しない。学習時に削除している??

得られた仮説をもとに、OoVを減らせるかトライしてみます。

前処理1(記号の処理をいい感じにする)

記号は削除すべきか・それとも残すべきかは、場合によります。ゴールデンルールに従えば、利用する外部Embeddingの単語として存在する場合には残せばよいし、存在しない場合は削除した方がよいです。データセットのVocabをできるだけ外部Embedddingの単語に近づけていきます。

今回のデータで?&をについて確認してみます。次の結果からは、?についてはVocabの単語から削除し、&については外部Embeddingに存在するので残しておくべきということになります。これらは利用する外部Embeddingデータによって前処理は異なるため、判断は変わってきます。

'?' in embeddings_index
False
'&' in embeddings_index
True

そんなんで、クレンジングしていきます。&は語彙として残しますが、他は削除します。

def clean_text(x):
    x = str(x)
    for punct in "/-'":
        x = x.replace(punct, ' ')
    for punct in '&':
        x = x.replace(punct, f' {punct} ')
    for punct in '?!.,"#$%\'()*+-/:;<=>@[\\]^_`{|}~' + '“”’':
        x = x.replace(punct, '')
    return x

text_clean = text.progress_apply(lambda x: clean_text(x))
sentences = text_clean.apply(lambda x: x.split())
vocab = build_vocab(sentences)
oov = check_coverage(vocab, embeddings_index)
Found embeddings for 57.38% of vocab
Found embeddings for  89.99% of all text

Quoraのテキストデータから得られた単語の57.38%が、外部Embeddingデータにも含まれるようになりました。前回の24.31%から大きく増加しました! さらに、再度OoVをチェックしてみます。

oov[:20]
[('to', 406298),
 ('a', 403852),
 ('of', 332964),
 ('and', 254081),
 ('2017', 8781),
 ('2018', 7373),
 ('10', 6642),
 ('12', 3694),
 ('20', 2942),
 ('100', 2883),
 ('15', 2762),
 ('12th', 2551),
 ('11', 2356),
 ('30', 2163),
 ('18', 2066),
 ('50', 1993),
 ('16', 1589),
 ('14', 1533),
 ('17', 1505),
 ('13', 1390)]

語尾に?がついた単語については先ほどの前処理によってなくなりましたが、新たな仮説が導かれます。

  • OoVに数字がたくさんある。Google news側で、数字に対するなにかしらの前処理が行われているのでは?

前処理2(数字の処理をいい感じにする)

今回も、データセットのVocabをできるだけ外部Embedddingの単語に近づけていきます。そのためにEmbedding側でどのように数字が処理されているかを探索的に探っていきます。 結論としては、利用したGoogle newsのEmbeddingでは、数字は#に置き換えられているようです。10であれば##99.99であれば##.##といった具合です。

データセット側も同様の処理を行います。

import re

def clean_numbers(x):
    x = re.sub('[0-9]{5,}', '#####', x)
    x = re.sub('[0-9]{4}', '####', x)
    x = re.sub('[0-9]{3}', '###', x)
    x = re.sub('[0-9]{2}', '##', x)
    return x

text_clean2 = text_clean.progress_apply(lambda x: clean_numbers(x))
sentences = text_clean2.progress_apply(lambda x: x.split())
vocab = build_vocab(sentences)
oov = check_coverage(vocab, embeddings_index)
Found embeddings for 60.41% of vocab
Found embeddings for  90.75% of all text

さらに、60.41%と改善しました!

こんな感じで、前処理をやっていきます。

そのほか前処理していること

この他にも、OoVをもとにEmbeddingに近づけるためのアイデアを探っていきます。割愛しますが、Kernekでは以下のような処理も行っています。

  • QuoraのQ6Aのテキストデータには、ソーシャルメディアならではの人間が間違いやすいスペルミスが存在している(おそらくGoogle news側では少ない)。これをいい感じにしてやる。
  • OoVに多い「a」、「to」、「and」、「of」という単語を削除する。Google newsのEmbeddingを学習するときには明らかにこれらの単語をダウンサンプリングしている。

最後に

なんか間違ってたり、よりよいアイデアあれば教えてください。