WindowsからSCDVを使ってみる

参考にさせて頂いたページ
qiita.com
2018年2月現在SCDVに関して日本語で書かれたページはここしか見つからなかった。
SCDVを勉強したいが元論文は英語で書かれていてもちろん読めるわけがない。
Githubスクリプトを眺めてもなにがなんだかわからない。
そういった素人の自分が実際に動かしながらSCDVを理解しようと試みた。
そのため、Pythonスクリプトはほぼそのまま使用させて頂いた。

環境

Windows10 pro 64bit
UbuntuWindowsストアからインストール)
Python 3.5.2(Ubuntuに最初からインストールされている)

下準備

  • pip3のインストール
sudo apt-get install python3-pip
  • pip3のアップデート
sudo pip3 install --upgrade pip
  • Jupyter Notebookのインストール(必須ではない)
sudo pip3 install jupyter

普通にJupyter Notebookが使用できることに驚いた。

パッケージのインストール

「pandas」を入れると「numpy」も同時に入る

sudo pip3 install pandas
sudo pip3 install scipy
sudo pip3 install gensim
sudo pip3 install scikit-learn

MeCabの導入

sudo apt-get install mecab
sudo apt-get install libmecab-dev
sudo apt-get install mecab-ipadic
sudo apt-get install mecab-ipadic-utf8
sudo pip3 install mecab-python3

テキストファイルの準備

(注意)エンコードは「UTF-8

wget http://www.rondhuit.com/download/ldcc-20140209.tar.gz
tar axvf ldcc-20140209.tar.gz

「text」フォルダが作成され、その中には9個のフォルダが存在する。
フォルダを一つずつあけて、「LICENSE.txt」を削除していく。
(もちろん削除する前に中身はきちんと読みましょう)

テキストファイルからデータフレームを作成

import pandas as pd
import glob
import pickle

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 dirlist:
    path = "./text/"+i+"/*.txt"
    files = glob.glob(path)
    for j in 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)
        
pickle.dump(df,open("textdata","wb"))
>>> type(df)
<class 'pandas.core.frame.DataFrame'>
>>> df.shape
(7367, 2)

7367行のデータフレームを「textdata」という名前で保存した。

単語ベクトルの作成(word2vec)

あらかじめ「model」というフォルダを作成しておく。

import logging
from gensim.models import Word2Vec
import MeCab
import re
import pickle

df = pickle.load(open("textdata","rb"))

tokenizer =  MeCab.Tagger("-Owakati")  
sentences = []

print ("Parsing sentences from training set...")
# Loop over each news article.
for review in df["news"]:
    try:
        # Split a review into parsed sentences.
        result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
        result = re.sub(r'[01234567890123456789!@#$%^&\-|\\*\“()_■×※⇒—●(:〜+=)/*&^%$#@!~`){}…\[\]\"\'\”:;<>?<>?、。・,./『』【】「」→←○]+', "", 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)

print ("Training Word2Vec model...")
# Train Word2Vec model.
model = Word2Vec(sentences, workers=40, hs = 0, sg = 1, negative = 10, iter = 25,\
            size=200, min_count = 20, \
            window = 10, sample = 1e-3, seed=1)

model_name = "word2vec"
model.init_sims(replace=True)

print ("Saving Word2Vec model...")
# Save Word2Vec model.
model.save("./model/"+model_name)
>>> type(model)
<class 'gensim.models.word2vec.Word2Vec'>
>>> type(model.wv.index2word)
<class 'list'>
>>> model.wv.index2word[0:10]
['の', 'に', 'を', 'が', 'は', 'て', 'で', 'た', 'と', 'し']
>>> len(model.wv.index2word)
11856
>>> type(model.wv.vectors)
<class 'numpy.ndarray'>
>>> model.wv.vectors.shape
(11856, 200)

「model」フォルダに「word2vec」という名前で保存した。

単語ベクトルのクラスタリング

import pickle
from gensim.models import Word2Vec
from sklearn.mixture import GaussianMixture

model = Word2Vec.load("./model/word2vec")
word_vectors = model.wv.vectors

clf =  GaussianMixture(n_components=60,
                       covariance_type="tied", init_params='kmeans', max_iter=50)
clf.fit(word_vectors)
idx_proba = clf.predict_proba(word_vectors)

idx_proba_dict = dict(zip( model.wv.index2word, idx_proba ))

pickle.dump(idx_proba_dict, open("./model/idx_proba_dict.pkl", "wb"))

print ("Clustering Done...")

#idx = clf.predict(word_vectors)
#idx_dict = dict(zip( model.wv.index2word, idx ))
#pickle.dump(idx_dict, open("./model/idx_dict.pkl", "wb"))

「idx」はそれぞれの単語がどのクラスタに属するかを表した配列
「idx_proba」はそれぞれの単語が各クラスタに属する確率

>>> type(idx)
<class 'numpy.ndarray'>
>>> idx.shape
(11856,)
>>> type(idx_proba)
<class 'numpy.ndarray'>
>>> idx_proba.shape
(11856, 60)
>>> sum(idx_proba[0,])
1.0
>>> sum(idx_proba[100,])
1.0000000000000042
>>> sum(idx_proba[200,])
0.99999999999999056
>>> sum(idx_proba[300,])
1.0000000000000282
>>> idx[100:110,]
array([56, 11,  9, 11, 48, 56, 57, 56, 56, 56])
>>> idx_proba[100:110,].argmax(axis=1)
array([56, 11,  9, 11, 48, 56, 57, 56, 56, 56])

「idx_proba」を単語に紐づけした辞書型「idx_proba_dict」 として保存した。
「idx」はこの先使用しない。

TF-IDFを計算(必要なのはIDFのみ)

import pickle
import numpy as np
import MeCab
import re
from sklearn.feature_extraction.text import TfidfVectorizer,HashingVectorizer

df = pickle.load(open("textdata","rb"))

tokenizer =  MeCab.Tagger("-Owakati")  
traindata = []

for review in df["news"]:
    result = tokenizer.parse(review).replace("\u3000","").replace("\n","")
    result = re.sub(r'[01234567890123456789!@#$%^&\-|\\*\“()_■×※⇒—●(:〜+=)/*&^%$#@!~`){}…\[\]\"\'\”:;<>?<>?、。・,./『』【】「」→←○]+', "", 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_

word_idf_dict = dict(zip(featurenames, idf))

pickle.dump(word_idf_dict, open("./model/word_idf_dict.pkl", "wb"))

単語に紐づけした辞書型「word_idf_dict」として保存した。
ここでの計算式は「log( (文章の総数+1) / (単語を含む文書数+1) ) + 1」で計算されている(自然対数)。

新しい単語ベクトルを求める

import numpy as np
import pickle
from gensim.models import Word2Vec

model = Word2Vec.load("./model/word2vec")
idx_proba_dict = pickle.load(open("./model/idx_proba_dict.pkl","rb"))
word_idf_dict = pickle.load(open("./model/word_idf_dict.pkl","rb"))

prob_wordvecs = {}
for word in idx_proba_dict:
    prob_wordvecs[word] = np.zeros( 60 * 200, dtype="float32" )
    for index in range(0, 60):
        try:
            prob_wordvecs[word][index*200:(index+1)*200] = model[word] * idx_proba_dict[word][index] * word_idf_dict[word]
        except:
            continue
            
pickle.dump(prob_wordvecs,open("./model/prob_wordvecs.pkl","wb"))
>>> type(prob_wordvecs)
<class 'dict'>
>>> len(prob_wordvecs)
11856
>>> prob_wordvecs["明日"].shape
(12000,)

単語ベクトルが12000次元になっていることがわかる。