word2vecとは?
自然言語処理の分野で用いられる、ニューラルネットワークを用いた単語の分散表現を獲得する手法となります。
自然言語処理では、単語間の意味を把握するために各単語をベクトルに置き換える処理が一般的に行われています。ベクトルは大きさと方向を持つ値なので、各単語がベクトル空間上に正しい方向と大きさを持っている場合、例えば王様というベクトルから男というベクトルを引くと、嬢王というベクトルに変換可能となります。
そのように全ての単語をベクトル化できると、単語同士の関連性を定量的に表すことができるので、単語の意味のようなものを扱えるようになります。
分散表現を獲得する方法としては、カウントベースの手法と推論ベースの手法があります。カウントベースの手法は、ある文章から単語の共起行列を作成して、次元削減を行います。
簡単な紹介になりますが、例えば「I have a pen」という文章がある場合、単語 “I” は “have”が、単語”have”では”I”と”a”という単語が周囲にあるという情報をカウントしていくと行列が作成できるので、この行列を次元削減することで分散表現を得る方法になります。
カウントベースの手法は、学習する文章が長い場合、計算コストが高い他、新しい単語を学習させるのに一から行列計算を行う必要があるのがデメリットとなります。
一方、推論ベースの手法では、ニューラルネットワークを用いて単語を予測させることで分散表現を自動的に作成します。
先程の例だと I have a penという文章がある場合、I ( ? ) a pen という入力を作成して?がhaveになるように学習を行うということになります。(これ以外にも、(?) have (?) penという文章から?を予測する方法もあります)
このようにして学習した後に得られる重みが、単語の分散表現となるので、重みを保存しておいて新規の学習で再学習させたりすることが可能となります。
また、一見推論ベースの方が高度でベクトル化も正しくできそうな感じがしますが、カウントベースと推論ベースどちらがより良い分散表現が得られるか、ということに関しては決着がついていないようです。
word2vecの使い方
この技術は自然言語処理の分野で用いられるので、文章情報を扱わない構造化データの場合は一見使いどころがなさそうですが、コンペではカテゴリー変数の情報をエンコードする手法として使われていたりします。
データセットとして売買代金がある程度大きい東証一部の銘柄の寄り引けの動きを使います。
2000年からの約20年間の寄り引けの値動きのデータがあるので、損益を分位数で4分割して、日付毎に銘柄を抽出します。

銘柄群に対してword2vecを使用することで、日毎に似た損益を出す対象のベクトルが同じ向きになると想定しています。モデルは上記のようなリスト形式に変換した後、下記の要領で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
import multiprocessing from gensim.models import Word2Vec core =multiprocessing.cpu_count() params=dict(size=100, #ベクトルの次元数 min_count=1, # 無視する頻度の閾値 window=1, # 単語の前後何個から予測を行うか workers=core) # 使用コア数 w2v_model = Word2Vec(**params) w2v_model.build_vocab(df_group["銘柄"].tolist(), progress_per=100) w2v_model.train(df_group["銘柄"].tolist(), total_examples=w2v_model.corpus_count,epochs=100) |
各種パラメータに関してはこちらから参照可能です。
学習された分散表現から、最も似ている対象を取り出すと…

同じ業種の銘柄程、損益が似ているのは感覚的にも合っているので良い感じかと思います。次に、最も似ていない対象を取り出すと…

こちらは、正しく抽出できているかを判断するのは難しいですが、値の大きさがpositiveの場合と比較して小さいことから、基本的に各銘柄はある程度似た動きをするということがわかります。
次に、作成した各銘柄の分散表現を取り出してみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
name_list = list(w2v_model.wv.vocab.keys()) for i, name in enumerate(name_list): if i ==0: stack = w2v_model.wv[name] else: stack = np.vstack((stack, w2v_model.wv[name])) stack_df = pd.DataFrame(stack) stack_df.columns = ["Dim_"+str(c) for c in stack_df.columns] stack_df["銘柄"] = name_list stack_df.set_index("銘柄") |
w2v_modelのオブジェクトから直接ベクトルの値を取り出すと、何故かkeyの順番が変わっていたので手間ですが、for文を使用しています。

上記のように取り出した後は、そのままモデルに入力してもよいですが、次元が増えると過学習する可能性が上がるので、次に紹介するt-SNEで次元削減してから入力としても良いかと思います。
可視化による確認
t-SNEを用いて可視化を行い、ベクトル化がどのように行われているのかを確認します。各銘柄は業種ラベルを持ち、同じ業種は同じ動き方をすることが多いので、業種毎のクラスターができていれば、分散表現として優秀なのかなと思います。
最初に先ほど計算したモデルを銘柄が多い業種を抽出して可視化します。
t-sneの計算はデータ量が大きいとかなり重たいので、GPUが使用できる場合はrapidsのdocker imageを使用するのがお勧めです。
1 2 3 4 5 6 7 8 9 |
from cuml.manifold import TSNE def get_tsne(X): X_reshape = X.reshape(len(X),-1) tsne = TSNE(n_components=2 , init='pca', n_iter=2500, random_state=23) X_reshape_2D = tsne.fit_transform(X_reshape) return X_reshape_2D train_2D = get_tsne(stack_df[features].values) |
色ごとに業種を分けているのですが、見た目的には50次元だとクラスターが形成されているように見えます。特徴量として有用なのか?ということに関しては、実際に入力する必要があると思いますが、可視化時にクラスターを形成していないようであれば入力としても使えないのではないかと思われます。
冒頭少し書きましたが、word2vecには2種類の学習のさせ方があります。CBOW(continuous bag-of-words)とskip-gramという方法です。前者は周辺の単語から、中心の単語を予測させる方法で、後者は中心の単語から周辺の単語を予測させる方法です。
skip-gramの方が学習難易度としては高く、得られる分散表現も良いものができるようなので、そちらでも試してみます。デフォルトではCBOWで学習がされるのでパラメータを指定してやります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import multiprocessing from gensim.models import Word2Vec core =multiprocessing.cpu_count() params=dict(size=100, #ベクトルの次元数 min_count=1, # 無視する頻度の閾値 window=1, # 単語の前後何個から予測を行うか sg=1, # skip-gram=1, cbow=0 workers=core) # 使用コア数 w2v_model = Word2Vec(**params) w2v_model.build_vocab(df_group["銘柄"].tolist(), progress_per=100) w2v_model.train(df_group["銘柄"].tolist(), total_examples=w2v_model.corpus_count,epochs=100) |
skip-gramの方は次元が少ないほうが、より良く分けられているように見えますが微妙ですね…。特徴量としての有用性については、実際のコンペで使用してみようと思います。
まとめ
推論ベースの手法を用いたエンコーディング手法を用いましたが、データによってはカウントベースでカテゴリーの共起行列を作成して特定の法則で変換する手法(LDA, tf-idf)の方が特徴量として効くこともあるようです。
ここら辺は、冒頭に書いたように推論ベースとカウントベースどちらの精度が良いか(分散表現としてどちらが良いか)はデータに依存するため両方行う必要がありそうです。
今回のコードはコンペ用の特徴量生成パイプラインに組み入れて、次はLDAやtf-idfの方を調べてみたいと思います。