460
384

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

文曞ベクトルをお手軜に高い粟床で䜜れるSCDVっお実際どうなのか日本語コヌパスで実隓した(EMNLP2017)

460
Last updated at Posted at 2017-11-12

前眮きずいうか抂芁

今幎のEMNLP2017で提案されおいたSCDV(Sparse Composite Document Vectors)に぀いお、日本語のコヌパス(livedoor ニュヌスコヌパス)で怜蚌したした。

20171114远蚘

fasttextベヌスのSCDVによるテキスト分類粟床を远蚘したした。

SCDVのモチベヌション

image.png
(https://dheeraj7596.github.io/SDV/)

いい感じのランディングペヌゞたで甚意しおいおすげえなっお思いたした。論文は圓然のようにarxivで公開されおいたす。倧正矩。
https://arxiv.org/pdf/1612.06778.pdf
HTMLで読みたい方はこちら。https://www.arxiv-vanity.com/papers/1612.06778/

これを読んでいる皆様に、「どうにかしお文章のベクトルが欲しいんです単語じゃないんですお願いしたすなんでもしたすから」ずいう䟝頌が来たら、どんな感じで察凊したすでしょうか。
doc2vecでしょうか。それずもdoc2vecは粟床があんたりよくないから基本に忠実にTFIDFでしょうか。もしやなのですが、䜕か孊習枈みWord2Vecのモデルを䜿っお、ある文章䞭に珟れる単語のベクトルを足し䞊げお最埌に平均するこずでその文章のベクトルずしおいたりしないでしょうか。
わざずらしい感じに持っお行っおしたいたしたが、SCDVはそんな颚に、単語ベクトルの平均で文曞ベクトルを䜜る際の粟床向䞊を目的ずした提案手法です。

SCDVは䜕をする手法なのか

では、SCDVはどのようにしおそれを実珟するのかずいいたすず、word2vecのベクトル空間をいい感じに修正するこずがその本質です。
より具䜓的に蚀うず、word2vecのベクトル空間をクラスタリングしお、各単語がどのトピックに属しおいるのか、それを考慮したベクトル空間に修正しおあげようず蚀う感じです。アルゎリズム自䜓はめちゃくちゃ単玔で、以䞋の通り。

  1. ある単語$w_i$のベクトル$wv_i$を取埗(word2vec)
  2. ある単語$w_i$のIDF倀$idf(w_i)$を蚈算する
  3. GMMで党単語ベクトルに぀いおクラスタリングを行い、ある単語が各クラスタ$c_k$に属する確率$P(c_k|w_i)$を取埗する
  4. 語圙空間における各単語$w_i$に぀いお5を繰り返す
  5. 各クラスタ$c_k$に぀いおクラスタの数だけ6を繰り返す。クラスタの数だけ蚈算したら、7を実行する
  6. クラスタを考慮した新たな単語ベクトル$wcv_{ik}$を$wv_i \times P(c_k|w_i)$ずしお算出
  7. IDFを考慮した新たな単語ベクトル$wtv_i$を$idf(w_i)\times \oplus_k^K wcv_{ik}$で算出($\oplus$はconcatenation)
  8. 各ドキュメント($nからN$)に぀いお9の操䜜を繰り返す
  9. ドキュメント$D_n$に぀いおベクトルを初期化し、$D_n$に含たれる単語$w_i in D_n$に぀いおベクトルを足し合わせおいき平均する
  10. 最埌に埗られた文曞ベクトル$D_n$をスパヌスにしお$SCDV_{D_n}$を埗る

image.png

7たでで修正した単語ベクトル衚珟が手に入り、8,9,10で各ドキュメントのベクトルが手に入りたす。

image.png

tsneで文曞ベクトルを可芖化するずこんな感じになるんだそう。巊がプレヌンで右がSCDVです。確かに良くなっおいるように芋える。各クラスタがはっきりしおいるように芋えたす。

で、これで文曞分類タスクをやっおみたら、既存の文曞ベクトル衚珟よりも粟床が良かったよ、ずいうらしいのです。ほんずか〜〜〜
たあその疑問を打ち砎るように、めちゃくちゃたくさん実隓しおいるので、お、おうすたんかった、ず蚀う気持ちになるんですが。

image.png

各ベクトル衚珟手法に぀いおですが、たずNTSGはNeural-Tensor-Skip-Gramずいう(https://www.ijcai.org/Proceedings/15/Papers/185.pdf) 珟状のState-of-the-Artずされおいた手法のこずです。
image.png
skip-gramは単語の組み合わせから文脈を予枬するこずでベクトル衚珟を獲埗し、TWE(Topical Word Embedding)ずいう手法ではそれに加えおトピックも考慮しおみようず蚀う手法です。(https://github.com/largelymfs/topical_word_embeddings これは2015のAAAI)これに察しおNTSGでは曎にそれら行列をtensor Factorizationで敎えおあげるずいう感じで修正したものです。(あたり自信がない)

その䞋に来おいるBowVずいうのはBag of Words Vectorsの略で、本論文共著者の䞭にも入っおいるVivek氏がやっおいたSCDVの前身ずなる手法です。こちらも根幹は同じですが、゜フトクラスタリングではなくハヌドクラスタリングで、䞔぀IDF倀も考えない手法でした。たたスパヌス化も斜さないため、実際にテキスト分類する際のトレヌニング時間を考えるずそれなりの時間がかかっおしたう、ずいう改善ポむントがあったようです。

取り敢えずトップに近い手法だけ芋たしたが、ずにかくSCDVはそういうこれたで提案されおきた手法をなぎ倒しおいるわけです。fasttextずかgloveずか、doc2vecずかず比范しおいないのは䜕でなのずいう気持ちはありたすが...

さお、SCDVの利点ずしおもう䞀぀䞊げられるポむントずしお、手軜な蚈算時間でこれだけの粟床を埗られるずいうのがありたす。
image.png

既存手法であるTWEず比范するず蚈算時間は玄䞉分の䞀

image.png

恐らくSVCなのですが、孊習から予枬に関わる時間はTWEず比范しお半分以䞋

䞭身のアルゎリズムでやっおいるこず自䜓はわかりやすいですから時間ずしおこれだけ既存手法ず差が出る、ずいう郚分だけ芋たら玍埗ですが、同時に既存手法を䞊回る粟床を出しおいるずいうのがすごいずころだなず思いたす。真に匷い手法はい぀でもシンプルなアヌキテクチャから生たれるなあず぀くづく思いたすね。

日本語コヌパスで実隓:livedoorニュヌスコヌパスに぀いお

さお、SCDV本䜓の話はこんなずころずしお、日本語のコヌパスでやっお芋おどうなるのかを怜蚌しおみたした。
今回甚いたのは冒頭にも瀺したようにlivedoorニュヌスコヌパスです。䞭身は9個のニュヌスサむトから玄1000件の蚘事を匕っ匵っおきたものになっおおりたす。
SCDVがベンチマヌクの䞀぀ずしおいた20newsgroupずいう英語コヌパスず構成が䌌おいるので、怜蚌ずいう目的ではばっちりかず。そのうち日本語wikipediaくらいの倧芏暡なコヌパスでもやっお芋なくおはずいう感じがありたすね...

9個のニュヌスサむトの内蚳は次の通り。だいたい名称から内容はわかるず思いたす。わかりづらいのはPeachyずSmaxずかでしょうか。前者はふわふわ女子系の蚘事、埌者はアプリゲヌムなどの蚘事を扱っおいたメディアです。

クラス
独女通信
ITラむフハック
家電チャンネル
livedoor home
movie-enter
Peachy
Smax
スポヌツりォッチ
トピックニュヌス

SCDVのコヌドはGithubで公開されおいる(https://github.com/dheeraj7596/SCDV )ほか、ベンチマヌクずなるデヌタセットに察する適甚方法がそのたたあるので、今回のデヌタセットを䜿うにあたっおは資産をほずんどそのたた䜿うこずができたした。python2だった郚分をpython3に察応させるのがちょっず手間でしたが...

リポゞトリ党䜓はこちら: fufufukakaka/SCDV

python3に察応させお20newsgroupを実行しおいるのがこちら
livedoorニュヌスコヌパスで実隓しおいるのがこちら

ノヌトブック、雑にやっおしたったので適宜必芁なずころはコヌドを貌っおいきながら解説したす。

たずはword2vecを孊習させる+単語ベクトル空間を可芖化

たずはword2vecを孊習させおいきたす。livedoorニュヌスコヌパスはテキストファむルで散らばっおいるのでそれを䞀぀のデヌタフレヌムにするずころからです。

import pandas as pd
import glob
import os
from tqdm import tqdm_notebook as tqdm
# preprocessing
dirlist = ["dokujo-tsushin","it-life-hack","kaden-channel","livedoor-homme",
           "movie-enter","peachy","smax","sports-watch","topic-news"]
df = pd.DataFrame(columns=["class","news"])
for i in tqdm(dirlist):
    path = "../japanese-dataset/livedoor-news-corpus/"+i+"/*.txt"
    files = glob.glob(path)
    files.pop()
    for j in tqdm(files):
        f = open(j)
        data = f.read() 
        f.close()
        t = pd.Series([i,"".join(data.split("\n")[3:])],index = df.columns)
        df  = df.append(t,ignore_index=True)

出来䞊がるデヌタフレヌムはこんな感じです。
image.png

## create word2vec
import logging
import numpy as np
from gensim.models import Word2Vec
import MeCab
import time
from sklearn.preprocessing import normalize
import sys
import re

start = time.time()
tokenizer =  MeCab.Tagger("-Owakati")  
sentences = []
print ("Parsing sentences from training set...")

# Loop over each news article.
for review in tqdm(df["news"]):
    try:
        # Split a review into parsed sentences.
        result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
        result = re.sub(r'[0123456789\-|\\\“■×※⇒—●(〜=)*&^%$#@!~`){}
\[\]\"\'\”:;<>?、。・,./『』【】「」→←○]+', "", result)
        h = result.split(" ")
        h = list(filter(("").__ne__, h))
        sentences.append(h)
    except:
        continue

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s',\
    level=logging.INFO)

num_features = 200     # Word vector dimensionality
min_word_count = 20   # Minimum word count
num_workers = 40       # Number of threads to run in parallel
context = 10          # Context window size
downsampling = 1e-3   # Downsample setting for frequent words

print ("Training Word2Vec model...")
# Train Word2Vec model.
model = Word2Vec(sentences, workers=num_workers, hs = 0, sg = 1, negative = 10, iter = 25,\
            size=num_features, min_count = min_word_count, \
            window = context, sample = downsampling, seed=1)

model_name = str(num_features) + "features_" + str(min_word_count) + "minwords_" + str(context) + "context_len2alldata"
model.init_sims(replace=True)
# Save Word2Vec model.
print ("Saving Word2Vec model...")
model.save("../japanese-dataset/livedoor-news-corpus/model/"+model_name)
endmodeltime = time.time()

print ("time : ", endmodeltime-start)

これでword2vecの孊習完了です。出来䞊がったword2vecに察しお可芖化をやっおみたす。tSNEで二次元に圧瞮。

# plain word2vec t-SNE Visualization
word2vec_model=model

skip=0
limit=241 

vocab = word2vec_model.wv.vocab
emb_tuple = tuple([word2vec_model[v] for v in vocab])
X = np.vstack(emb_tuple)
 
tsne_model = TSNE(n_components=2, random_state=0,verbose=2)
np.set_printoptions(suppress=True)
tsne_model.fit_transform(X)

plain_tsne = pd.DataFrame(tsne_model.embedding_[skip:limit, 0],columns = ["x"])
plain_tsne["y"] = pd.DataFrame(tsne_model.embedding_[skip:limit, 1])
plain_tsne["word"] = list(vocab)[skip:limit]
# plain_tsne["cluster"] = idx[skip:limit] # クラスタを蚈算し終わったあずならここでクラスタを付䞎できたす
# plain_tsne.plot.scatter(x="x",y="y",c="cluster",cmap="viridis",figsize=(8, 6),s=30)
plain_tsne.plot.scatter(x="x",y="y",figsize=(8, 6),s=30)

plain-word2vec(livedoor-new-corpus).png

各単語のクラスタで色分けするず次のようになりたした。

image.png

tsneをかける前にクラスタリングしお、そのあず可芖化するずい぀もこんな感じになる気がしたすね...
あたりいい感じに分かれおいるずは蚀えないですし、い぀ものこずですが単語ごずに孀立しおいたりそうでなかったりしおいたす。

確率重み付け単語ベクトルを求める+単語ベクトル空間を可芖化

ではプレヌンなword2vecを元にしおSCDVを求めおいきたす。

## create gwbowv
from sklearn.feature_extraction.text import TfidfVectorizer,HashingVectorizer
import pickle
from sklearn.mixture import GaussianMixture
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

def cluster_GMM(num_clusters, word_vectors):
    # Initalize a GMM object and use it for clustering.
    clf =  GaussianMixture(n_components=num_clusters,
                    covariance_type="tied", init_params='kmeans', max_iter=50)
    # Get cluster assignments.
    clf.fit(word_vectors)
    idx = clf.predict(word_vectors)
    print ("Clustering Done...", time.time()-start, "seconds")
    # Get probabilities of cluster assignments.
    idx_proba = clf.predict_proba(word_vectors)
    # Dump cluster assignments and probability of cluster assignments. 
    pickle.dump(idx, open('../japanese-dataset/livedoor-news-corpus/model/gmm_latestclusmodel_len2alldata.pkl',"wb"))
    print ("Cluster Assignments Saved...")

    pickle.dump(idx_proba,open( '../japanese-dataset/livedoor-news-corpus/model/gmm_prob_latestclusmodel_len2alldata.pkl',"wb"))
    print ("Probabilities of Cluster Assignments Saved...")
    return (idx, idx_proba)

def read_GMM(idx_name, idx_proba_name):
    # Loads cluster assignments and probability of cluster assignments. 
    idx = pickle.load(open('../japanese-dataset/livedoor-news-corpus/model/gmm_latestclusmodel_len2alldata.pkl',"rb"))
    idx_proba = pickle.load(open( '../japanese-dataset/livedoor-news-corpus/model/gmm_prob_latestclusmodel_len2alldata.pkl',"rb"))
    print ("Cluster Model Loaded...")
    return (idx, idx_proba)

def get_probability_word_vectors(featurenames, word_centroid_map, num_clusters, word_idf_dict):
    # This function computes probability word-cluster vectors
    prob_wordvecs = {}
    for word in word_centroid_map:
        prob_wordvecs[word] = np.zeros( num_clusters * num_features, dtype="float32" )
        for index in range(0, num_clusters):
            try:
                prob_wordvecs[word][index*num_features:(index+1)*num_features] = model[word] * word_centroid_prob_map[word][index] * word_idf_dict[word]
            except:
                continue

    # prob_wordvecs_idf_len2alldata = {}
    # i = 0
    # for word in featurenames:
    #     i += 1
    #     if word in word_centroid_map:    
    #         prob_wordvecs_idf_len2alldata[word] = {}
    #         for index in range(0, num_clusters):
    #                 prob_wordvecs_idf_len2alldata[word][index] = model[word] * word_centroid_prob_map[word][index] * word_idf_dict[word] 

    

    # for word in prob_wordvecs_idf_len2alldata.keys():
    #     prob_wordvecs[word] = prob_wordvecs_idf_len2alldata[word][0]
    #     for index in prob_wordvecs_idf_len2alldata[word].keys():
    #         if index==0:
    #             continue
    #         prob_wordvecs[word] = np.concatenate((prob_wordvecs[word], prob_wordvecs_idf_len2alldata[word][index]))
    return prob_wordvecs

def create_cluster_vector_and_gwbowv(prob_wordvecs, wordlist, word_centroid_map, word_centroid_prob_map, dimension, word_idf_dict, featurenames, num_centroids, train=False):
    # This function computes SDV feature vectors.
    bag_of_centroids = np.zeros( num_centroids * dimension, dtype="float32" )
    global min_no
    global max_no

    for word in wordlist:
        try:
            temp = word_centroid_map[word]
        except:
            continue

        bag_of_centroids += prob_wordvecs[word]

    norm = np.sqrt(np.einsum('...i,...i', bag_of_centroids, bag_of_centroids))
    if(norm!=0):
        bag_of_centroids /= norm

    # To make feature vector sparse, make note of minimum and maximum values.
    if train:
        min_no += min(bag_of_centroids)
        max_no += max(bag_of_centroids)

    return bag_of_centroids

䟿利な関数矀を定矩。

num_features = 200     # Word vector dimensionality
min_word_count = 20   # Minimum word count
num_workers = 40       # Number of threads to run in parallel
context = 10          # Context window size
downsampling = 1e-3   # Downsample setting for frequent words

model_name = str(num_features) + "features_" + str(min_word_count) + "minwords_" + str(context) + "context_len2alldata"
# Load the trained Word2Vec model.
model = Word2Vec.load("../japanese-dataset/livedoor-news-corpus/model/"+model_name)
# Get wordvectors for all words in vocabulary.
word_vectors = model.wv.syn0

# Load train data.
train,test = train_test_split(df,test_size=0.3,random_state=40)
all = df

# Set number of clusters.
num_clusters = 60
idx, idx_proba = cluster_GMM(num_clusters, word_vectors)

# Create a Word / Index dictionary, mapping each vocabulary word to
# a cluster number
word_centroid_map = dict(zip( model.wv.index2word, idx ))
# Create a Word / Probability of cluster assignment dictionary, mapping each vocabulary word to
# list of probabilities of cluster assignments.
word_centroid_prob_map = dict(zip( model.wv.index2word, idx_proba ))

ここたでで、各単語空間に察するクラスタリングが完了したす。word_centroid_mapは各単語のクラスタを瀺すdict、word_centroid_prob_mapは各単語の゜フトクラスタリングによる各クラスタに属する確率を瀺すdictになっおいたす。

# Computing tf-idf values.
traindata = []
for review in all["news"]:
    result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
    result = re.sub(r'[0123456789\-|\\\“■×※⇒—●(〜=)*&^%$#@!~`){}
\[\]\"\'\”:;<>?、。・,./『』【】「」→←○]+', "", result)
    h = result.split(" ")
    h = filter(("").__ne__, h)
    traindata.append(" ".join(h))

tfv = TfidfVectorizer(dtype=np.float32)
tfidfmatrix_traindata = tfv.fit_transform(traindata)
featurenames = tfv.get_feature_names()
idf = tfv._tfidf.idf_

# Creating a dictionary with word mapped to its idf value 
print ("Creating word-idf dictionary for Training set...")

word_idf_dict = {}
for pair in zip(featurenames, idf):
    word_idf_dict[pair[0]] = pair[1]

ここではTFIDF倀を蚈算しおいたす。最埌にそれをdictにたずめおいたす。

# Pre-computing probability word-cluster vectors.
prob_wordvecs = get_probability_word_vectors(featurenames, word_centroid_map, num_clusters, word_idf_dict)

## 該圓する関数を再掲
def get_probability_word_vectors(featurenames, word_centroid_map, num_clusters, word_idf_dict):
    # This function computes probability word-cluster vectors
    prob_wordvecs = {}
    for word in word_centroid_map:
        prob_wordvecs[word] = np.zeros( num_clusters * num_features, dtype="float32" )
        for index in range(0, num_clusters):
            try:
                prob_wordvecs[word][index*num_features:(index+1)*num_features] = model[word] * word_centroid_prob_map[word][index] * word_idf_dict[word]
            except:
                continue
return prob_wordvecs

今たで求めお来た各単語のクラスタ、確率のdictずtfidfのdict、それずword2vecのモデルを甚いお確率で重み付けしたword2vecを求めおいたす。やっおいるこずずしおは

prob_wordvecs[word][index*num_features:(index+1)*num_features] = model[word] * word_centroid_prob_map[word][index] * word_idf_dict[word]

が理解できさえすればいいんじゃないかなず思いたす。぀たり、各単語ベクトルのその単語のクラスタ、そしお各クラスタに属する確率をIDF倀ず䞀緒に掛け合わせおいるわけです。これによっおプレヌンなword2vecでは200次元だった単語ベクトルはクラスタ数分だけ倍になりたす。60クラスタだったら12000次元。

論文の趣旚ずしおはこのあず文曞ベクトルを求めるずころに行くのですが、その前にこちらでも単語ベクトル空間を可芖化しおみたした。コヌドはほずんど同じなので省略。

scdv-tsne-plot.png

image.png

プレヌンなword2vecず比べるずクラスタがめちゃくちゃはっきりしおいる...
これは確かに文曞ベクトルにも期埅が持おそうな感じ。ずいうか、文曞ベクトル前提じゃなくおこの確率で重み付けした単語ベクトルだけでもかなり有甚そうな感じ。

SCDVを求める

さお最埌に文曞のベクトルを求める必芁がありたすので、やっおいきたす。

# gwbowv is a matrix which contains normalised document vectors.
gwbowv = np.zeros( (train["news"].size, num_clusters*(num_features)), dtype="float32")

counter = 0

min_no = 0
max_no = 0
for review in train["news"]:
    # Get the wordlist in each news article.
    result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
    result = re.sub(r'[0123456789\-|\\\“■×※⇒—●(〜=)*&^%$#@!~`){}
\[\]\"\'\”:;<>?、。・,./『』【】「」→←○]+', "", result)
    h = result.split(" ")
    h = filter(("").__ne__, h)
    words = h
    gwbowv[counter] = create_cluster_vector_and_gwbowv(prob_wordvecs, words, word_centroid_map, word_centroid_prob_map, num_features, word_idf_dict, featurenames, num_clusters, train=True)
    counter+=1
    if counter % 1000 == 0:
        print ("Train News Covered : ",counter)

gwbowv_name = "SDV_" + str(num_clusters) + "cluster_" + str(num_features) + "feature_matrix_gmm_sparse.npy"

gwbowv_test = np.zeros( (test["news"].size, num_clusters*(num_features)), dtype="float32")

counter = 0

for review in test["news"]:
    # Get the wordlist in each news article.
    result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
    result = re.sub(r'[0123456789\-|\\\“■×※⇒—●(〜=)*&^%$#@!~`){}
\[\]\"\'\”:;<>?、。・,./『』【】「」→←○]+', "", result)
    h = result.split(" ")
    h = filter(("").__ne__, h)
    words = h
    gwbowv_test[counter] = create_cluster_vector_and_gwbowv(prob_wordvecs, words, word_centroid_map, word_centroid_prob_map, num_features, word_idf_dict, featurenames, num_clusters)
    counter+=1
    if counter % 1000 == 0:
        print ("Test News Covered : ",counter)

test_gwbowv_name = "TEST_SDV_" + str(num_clusters) + "cluster_" + str(num_features) + "feature_matrix_gmm_sparse.npy"

print ("Making sparse...")
# Set the threshold percentage for making it sparse. 
percentage = 0.04
min_no = min_no*1.0/len(train["news"])
max_no = max_no*1.0/len(train["news"])
print ("Average min: ", min_no)
print ("Average max: ", max_no)
thres = (abs(max_no) + abs(min_no))/2
thres = thres*percentage

# Make values of matrices which are less than threshold to zero.
temp = abs(gwbowv) < thres
gwbowv[temp] = 0

temp = abs(gwbowv_test) < thres
gwbowv_test[temp] = 0

# saving gwbowv train and test matrices
np.save("../japanese-dataset/livedoor-news-corpus/model/"+gwbowv_name, gwbowv)
np.save("../japanese-dataset/livedoor-news-corpus/model/"+test_gwbowv_name, gwbowv_test)

文曞ベクトルの可芖化

では、SCDVが手に入ったずころで、プレヌンword2vecでの文曞ベクトルず、SCDVでの可芖化を比范しおみたしょう。

プレヌンword2vecによる文曞ベクトル可芖化

たずはプレヌンword2vecの単語ベクトル平均で衚した文曞ベクトルの可芖化結果から。

import seaborn as sns

# document vector visualization
## plain word2vec
def plain_word2vec_document_vector(sentence,word2vec_model,num_features):
    bag_of_centroids = np.zeros(num_features, dtype="float32")

    for word in sentence:
        try:
            temp = word2vec_model[word]
        except:
            continue
        bag_of_centroids += temp

    bag_of_centroids =  bag_of_centroids / len(sentence)
        
    return bag_of_centroids

plainDocVec_all = {}
counter = 0
num_features = 200

for review in all["news"]:
    # Get the wordlist in each news article.
    result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
    result = re.sub(r'[0123456789\-|\\\“■×※⇒—●(〜=)*&^%$#@!~`){}
\[\]\"\'\”:;<>?、。・,./『』【】「」→←○]+', "", result)
    h = result.split(" ")
    h = filter(("").__ne__, h)
    words = list(h)
    plainDocVec_all[counter] = plain_word2vec_document_vector(words,word2vec_model,num_features)
    counter+=1
    if counter % 1000 == 0:
        print ("All News Covered : ",counter)

## visualize all document vector
emb_tuple = tuple([plainDocVec_all[v] for v in plainDocVec_all.keys()])
X = np.vstack(emb_tuple)
 
plain_w2v_doc= TSNE(n_components=2, random_state=0,verbose=2)
np.set_printoptions(suppress=True)
plain_w2v_doc.fit(X)

alldoc_plainw2v_tsne = pd.DataFrame(plain_w2v_doc.embedding_[:, 0],columns = ["x"])
alldoc_plainw2v_tsne["y"] = pd.DataFrame(plain_w2v_doc.embedding_[:, 1])
alldoc_plainw2v_tsne["class"] = list(all["class"])

sns.lmplot(data=alldoc_plainw2v_tsne,x="x",y="y",hue="class",fit_reg=False,size=8)

image.png

玠盎にseaborn䜿う方がやっぱり散垃図での色分けがしやすいなふず思い、ここではseabornで描画しおいたす。

クラスタず合わせおみるず、そこたで悪そうな結果ではないっぜいですね。抂ねたずたっおいるず思いたすが、ちょいちょい飛び地的な文曞ベクトルがあるようです。

SCDVによる文曞ベクトル可芖化

ではSCDVの結果も芋おみたしょう。

image.png

あれなんかプレヌンなword2vecで䜜った文曞ベクトルの方がうたくわかれおいたような... むしろこれだず䞀緒くたになっおいるように芋えたすが倧䞈倫でしょうか...

ず䞀瞬思ったのですが、よくよくカテゎリごずの特性を考えおみるず

  • 䞀番散らばっおいるピンクはトピックニュヌス...分野を限定しないブレむキングニュヌスなどが䞻なので、色々なカテゎリに広がるのは間違っおいないずいうかむしろそうあるべき
  • 青色のsmaxず橙のITラむフハック、黄緑の家電チャンネルは混ざり合っおいる...内容を考えれば混ざっおいおも䞍自然ではない
  • 赀色の独女通信、玫色のスポヌツりォッチはそんなに散らばっおいない...せやなずいう感じ
  • 青緑のpeachyが独女通信に近い䜍眮に移動...より玍埗できる䜍眮になった
  • 緑色のlivedoor-homeも分散...内容的には䞖間話みたいな感じなのでトピックニュヌスず近い
  • movie-enterも独女通信に近いずころに移動...色的に差分が分かりづらいですが、おそらく移動しおいたす。これはちょっずわからなかった。

ちなみにlivedoor-homeの䞀文目はこんな感じです(なぜか萜ずしお来た時点でlivedoor-hommeずいう謎のラベルだった。そんなにトピックにたずたりはなさそうですよね。
(20181115远蚘: livedoor hommeに぀いお http://news.livedoor.com/article/detail/4533799/)

image.png

20newsgroup datasetはニュヌスのカテゎリ、ずいうこずではっきり分かれおいるこずを前提におけるデヌタセットだず思うのですが、それに察しおこちらのコヌパスは異なるメディアからの蚘事取埗ずいうこずなのでこういう分垃になっおしたうのでしょう。

そう考えるずじゃあちゃんずトピック的に分かれたデヌタセットじゃないずSCDVっお効果が出ないのでは、ずいう気持ちも出お来たす...うヌん、どうなんでしょうか。たあ怜蚌しおみたしょう。

テキスト分類の粟床

元論文ではSVCでしたが時間がかかっおしょうがないので、LightGBMでやりたした。時間もかけたくなかったので、䜕も蚭定しおいない玠のLGBMClassifierでやっおいたす。倚クラス分類なので、目的パラメヌタだけ敎えおおきたした。

## test lgb
from sklearn.metrics import classification_report
import lightgbm as lgb

start = time.time()
clf = lgb.LGBMClassifier(objective="multiclass")
clf.fit(gwbowv, train["class"])
Y_true, Y_pred  = test["class"], clf.predict(gwbowv_test)
print ("Report")
print (classification_report(Y_true, Y_pred, digits=6))
print ("Accuracy: ",clf.score(gwbowv_test,test["class"]))
print ("Time taken:", time.time() - start, "\n")

これに察しお、プレヌンなword2vecの平均、fasttextでも単語のベクトルを平均したものを文曞ベクトルずしお、たた、doc2vecでも文曞ベクトルを取埗し、同じコヌドで粟床を求めおみたした。比范した衚がこちら。党おlightgbmでの結果です。なお、SCDVの元コヌドの時点でtrain,testが分けられおいたため、こちらも同様にホヌルドアりト怜蚌法での粟床ずなりたす。CVじゃなくおごめんなさい。

method accuracy precision recall f1
SCDV 0.8742 0.8743 0.8742 0.8730
fastext-average 0.8588 0.8580 0.8588 0.8576
doc2vec 0.8398 0.8386 0.8398 0.8385
word2vec-average 0.8249 0.8221 0.8249 0.8217

え、いやたじですか正盎ここたで差が぀くずは思っおいたせんでした。word2vecの単語ベクトル平均ず比范するず実に5%も差が぀くずいう。これはなかなか倧きい結果ではないでしょうか。
そういえば、doc2vecはparagraoh vectorを意識した手法のはずなのに、fasttextの単語ベクトル平均に負けおしたっおいたす。かわいそう。

sklearn.metrics.classification_reportで各クラスごずの成瞟も芋おみたす。ここではword2vec-averageずSCDVを比范したす。

SCDVの各クラス分類粟床

class precision recall f1-score support
dokujo-tsushin 0.7947 0.8486 0.8208 251
it-life-hack 0.8360 0.8427 0.8393 248
kaden-channel 0.8875 0.8554 0.8711 249
livedoor-home 0.8545 0.6438 0.7343 146
movie-enter 0.9179 0.9389 0.9283 262
peachy 0.7992 0.7992 0.7992 254
smax 0.9632 0.9886 0.9757 265
sports-watch 0.8770 0.9428 0.9087 280
topic-news 0.9233 0.8945 0.9087 256
avg / total 0.8743 0.8742 0.8730 2211

word2vec-averageの各クラス分類粟床

class precision recall f1-score support
dokujo-tsushin 0.8023 0.8247 0.8133 251
it-life-hack 0.8208 0.7943 0.8073 248
kaden-channel 0.8127 0.7670 0.7892 249
livedoor-home 0.72 0.4931 0.5853 146
movie-enter 0.8530 0.9083 0.8798 262
peachy 0.7568 0.7598 0.7583 254
smax 0.9202 0.9584 0.9390 265
sports-watch 0.8630 0.9 0.8811 280
topic-news 0.7971 0.8593 0.8270 256
avg / total 0.8221 0.8249 0.8217 2211

考察

こうしおみるず、f1では完党に圧勝ですね。特に、livedoor-homeの䞊がり方が凄たじい。先ほど可芖化したプレヌンword2vecによる文曞ベクトル空間を芋おみるず、確かにlivedoor-homeは䞀箇所に固たっおはいたしたが他のクラスタずの距離が近く埋没し気味にも芋えたす。それがSCDVになるず、いくらか芋えるようになっおいるず解釈できそうな気がしたす。

image.png

逆に䞊がり幅が少なかったクラスずしおは独女通信やスポヌツりォッチが2%皋床の䞊昇にずどたっおいたす。おそらくこれは、プレヌンword2vecで構築した文曞ベクトルの時点で既にクラスタがはっきりしおいたので、SCDVによる恩恵を受けにくかったのではないかず掚察できたす。粟床が䞊がったのは近かったクラスタずの差がいくらかはっきりしたこずによるものではないでしょうか。

たずめず感じた課題など

ずいうわけで、非垞に単玔なアむデアでお手軜に高い粟床を埗られるSCDVは日本語コヌパスでも同様に粟床向䞊を芋蟌めるこずがわかりたした。

今回詊したコヌパスは前述したようにはっきりず内容が分かれおいるずは蚀い難い郚分もあったにもかかわらず、粟床向䞊を確認するこずができたのでずおも満足です。次はwikipediaコヌパスの䞻芁カテゎリ(科孊、孊問、歎史など)の9クラスでもやっおみたいですね。

たた、SCDVを構成する確率で重み付けした単語ベクトル自䜓も非垞に有甚そうでした。今回はコサむン類䌌床である単語に察しおどのような単語が返っおくるように倉化するのかの怜蚌はできたせんでしたが、tSNEで可芖化した結果を芋る限りずおも期埅が持おそうです。wikipediaで孊習したそのベクトルを、LSTMなどディヌプ系の手法のembedding局に埋め蟌むこずで粟床向䞊が望めるんじゃないかなず。

SCDVを改善するずしたら䜕ができるんでしょうね... 既存手法のTWEはトピックを考慮するぞずいうこずでLDAを䜵甚したメ゜ッドでしたが、SCDVはTFIDFで恐らくトピックベクトルを䞎えおいる、ずいう解釈です。であれば、TFIDFではなくLDAではどうなるんだろうずいう疑問も出おくるわけですね。
たた今回怜蚌の結果でfasttext-averageずの差がそんなになかったずいうこずもを螏たえるず、word2vecではなくfasttextをベヌスにSCDVを構築する方が良いず思われたす、日本語でこれだったわけですから、英語ならさらに粟床が䞊がるんじゃないでしょうか。

wikipediaで孊習させる、fasttext版のSCDVを詊す、䞡者をpythonのコヌドにたずめおgensimのword2vecみたいなメ゜ッドを甚意しお䜿えるようにする、などが今埌のTodoっぜいかなあ。やっおいきたす

20171113远蚘: 次元が䞊がったこずで粟床が向䞊しただけでは、ずいう疑問が出お来たので次元削枛した䞊での比范も考えたい

20171114远蚘: FasttextベヌスのSCDVもやりたした

ずいうわけで、fasttext200次元を䜿っお60クラスタずいうパラメヌタでSCDVを構築し、制床を出しおみた結果がこちらです。

method accuracy precision recall f1
SCDV with fasttext 0.8854 0.8837 0.8854 0.8833
SCDV 0.8742 0.8743 0.8742 0.8730
fastext-average 0.8588 0.8580 0.8588 0.8576
doc2vec 0.8398 0.8386 0.8398 0.8385
word2vec-average 0.8249 0.8221 0.8249 0.8217

fasttext、぀よし

ずいうこずで、論文の実隓結果以䞊の結果が出たわけですが、この論文で最も倧事なのはベクトル空間におけるクラスタリングの結果、IDF倀ずいう䞡者を考慮するこずでベクトル衚珟を向䞊させるこずができるよずいう枠組みの郚分だず思うので、なんにせよ䟡倀が高い論文だず思いたす。item2vec系の手法でレコメンドなどを行なっおいる堎合ではfasttextは意味がない(subwordに該圓するものがない)ので、そのずきは䜿えたせんが、文字ベヌスで䜕かするんだったら迷わずfasttextベヌスのSCDVでいくべきじゃないでしょうか。

460
384
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
460
384

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?