SCDV(Sparse Composite Document Vectors)とは文書の類似度を測るための手法(文書の特徴の表し方)です。文書の類似度を測るための手法は他にもいくつかある(たとえばBag of Wordsとかtf-idfとか)のですが、その中でも優れているということで最近使われ始めているようです。
こちらが元論文です。
https://arxiv.org/pdf/1612.06778.pdf
コードはこちらを参考に書いてみました。
https://www.pytry3g.com/entry/text-classification-SCDV
そもそもSCDVって何がすごいの?
Word2Vecを使って類義語問題を解決して(しようとして)いる点と、その単語群をクラスタリングすることで一定のトピックでまとめる点、さらにはSparse化することによって計算時間を減らして(へらそうとして)いる点があります。
少しやったことがある方はわかると思うのですが、自然言語処理において類義語問題は必ず出てきます。MacintoshとMacとかWindowsとWinとかを同じものと捉える必要がある、というお話です。それを解決するために類義語辞書を持ったりするのですが、準備が面倒だったりアップデートが面倒だったりします。Word2Vecはそこをある程度解決してくれる手法です。それをさらにクラスタリングしてトピック分類して使っています。
また、Sparse化の話はこちらのリンクなどを参照してもらえるのが早いかと。ざっくりいうと、小さな値は無視して計算すると、精度はあんまり下がらないのに計算時間はとっても速くなる、という話です。Sparseというのは「疎」という意味です。小さい値を無視して数値の分布を疎にするという意味だと理解しています。
https://developer.smartnews.com/blog/2017/06/sparse-neural-network/
実装時ハマった話
実装時にいろいろ苦労したので、残しておきます。
なお前述のサンプルコードをベースに説明します。
Mecab->Janome問題
この記事にも書いてあるんですが、Janomeはとっても導入しやすいです。サンプルコードはMecabで書かれていたのですが、インストールに手間取ってあきらめてJanomeにしてしまいました。
https://ushinji.hatenablog.com/entry/2017/11/23/161031
Word2VecのコーパスとSCDVのコーパスは違う
ここでWord2Vecのコーパスを読み込んでいるのですが、ここで渡すコーパスをSCDVで使うものと一致させないと、知らない単語が出てきて結果が期待と違ったものになってしまいます。
Word2Vecのコーパスがよりリッチなものなら、敢えて別のコーパスを使うという選択ももちろんありです。
sentences, train_x, test_x, train_t, test_t = dataset.load_corpus()
number of clustersにご注意
number of clustersというパラメータがあります。これはSCDVのクラスターの数を決めるパラメータです。ここを少なくしすぎてしまうと同じクラスターにたくさんの単語が分類されてしまい、類似した文書がたくさん出てきてしまう結果になると思われます。これはやってみた経験則ですが、コーパスのサイズが大きくなればなるほど、クラスターの数は増やしたほうがよさそうです。
parser.add_argument(
'--num_clusters', type=int, default=50
)
window_sizeは計算時間への影響が大きい
window_sizeというパラメータはどこまでの範囲の単語を「隣」と捉えるかの設定です。このパラメータがが大きければ大きいほどWord2Vecの計算時間が長くなります。経験的にはあまり大きくしても結果の改善には寄与しなかったので、1か2ぐらいが適切かなと思っています。
parser.add_argument(
'--window_size', type=int, default=2
)
メモリ消費で死んだ
ここで正規化された文書ベクトルの箱を作るのですが、コーパスサイズクラスターの数フィーチャーの数という計算になるので、ターゲットになるコーパスのサイズを増やしたり、前述のとおりそれに従ってクラスターの数を増やしたりすると、すぐに10GBとかいっちゃいます。ご安全に。
def make_gwbowv(self, corpus, train=True):
# gwbowv is a matrix which contains normalized document vectors.
gwbowv = np.zeros((len(corpus), self.num_clusters*self.num_features)).astype(np.float32)
その他わかったこと
Skip-gram
Skip-gramのパラメータを以下で設定します。
parser.add_argument(
'--sg', type=int, default=1
)
Skip-gramの意味は以下で書かれていますが、あんまり理解していないです。入力層から隠れ層への重みを設定する、というもののようです。むずい。
https://arxiv.org/pdf/1301.3781.pdf
SCDVの各次元は単語ではない
直感的には文書のベクトルというと単語のベクトルになりそうなのですがそうではなかったみたいです。その素性は、単語の特性のベクトルということらしいです。言い換えると、Word2Vecで単語をベクトル化するのですが、その次元がそのままSCDVの次元になります。
まとめ
エキスパートではないのでSCDVの意味自体を完全には理解していないと思うのですが、それでもいい感じに使えました。ちょっと頑張った文書処理をしたいのであれば、おためしあれ。