学習してないランダムなCNNでも特徴抽出の役に立つ

先月からディープラーニングを教えてくれる講座を受講しています。
今週からCNNに入ったのですが、先生が「CNNは特徴抽出器としてかなり優秀で、学習していないランダムなCNNでも高い精度が出せる」と教えてくれました。

「え、そうなの!?」とびっくりしましたので、MNISTを使って簡単に実験してみました。

まずはデータセットをロードして整形します。

import numpy as np

from keras.models import Sequential
from keras.layers import Conv2D, MaxPool2D, Dense, Flatten
from keras.losses import categorical_crossentropy
from keras.optimizers import Adam
from keras.metrics import categorical_accuracy
from keras.utils import to_categorical
from keras.datasets import mnist

# データセットのロード
(X_train, y_train), (X_test, y_test) = mnist.load_data()

train_samples, input_w, input_h = X_train.shape
test_samples = X_test.shape[0]
categories = len(np.unique(y_test))

# チャネル数 (1) を加える
X_train = X_train.reshape(train_samples, input_w, input_h, 1)
X_test = X_test.reshape(test_samples, input_w, input_h, 1)

# 0〜1に正規化
X_train = X_train.astype('float32') / 255
X_test = X_test.astype('float32') / 255

# 10次元のone-hotベクトル化
y_train = to_categorical(y_train, categories)
y_test = to_categorical(y_test, categories)

# 学習パラメータ
batch_size = 32
epochs = 10

比較対象となる全結合層でのみで構成したNNです。入力層 (784次元) -> 出力層 (10次元のソフトマックス) のみのシンプル過ぎなネットワークです。

  • パラメータ数は7850 (784×10 + 10) で、CNNを挟んでいない分、1層でもかなり多数になります
  • 精度は92.6%
model_nn = Sequential()

model_nn.add(Flatten(input_shape=(input_w, input_h, 1)))
model_nn.add(Dense(10, activation='softmax'))

model_nn.compile(
    loss=categorical_crossentropy, optimizer=Adam(), metrics=[categorical_accuracy]
)
print(model_nn.summary())
# _________________________________________________________________
# Layer (type)                 Output Shape              Param #   
# =================================================================
# flatten_18 (Flatten)         (None, 784)               0         
# _________________________________________________________________
# dense_49 (Dense)             (None, 10)                7850      
# =================================================================
# Total params: 7,850
# Trainable params: 7,850
# Non-trainable params: 0
# _________________________________________________________________

model_nn.fit(
    X_train, y_train, batch_size=batch_size, epochs=epochs, 
    validation_data=(X_test, y_test), verbose=1
)
# Train on 60000 samples, validate on 10000 samples
# Epoch 1/10
# 60000/60000 [==============================] - 6s 101us/step - loss: 0.4653 - categorical_accuracy: 0.8794 - val_loss: 0.3068 - val_categorical_accuracy: 0.9128
# ...
# Epoch 10/10
# 60000/60000 [==============================] - 6s 93us/step - loss: 0.2507 - categorical_accuracy: 0.9306 - val_loss: 0.2644 - val_categorical_accuracy: 0.9262

次に、特徴抽出器としてランダムなCNNを全結合層の前段に加えたネットワークを構成します。

  • CNNは畳み込み+プーリングの2セットの計4層構成となっており、7x7x8で出力されます
    • cnn_layers.trainable=Falseとすることで、学習させないようにしてます
  • 28x28x1から7x7x8に削減されたことで、パラメータ数も約半分となってます
  • 精度は94.2%
# ランダムなCNN
cnn_layers = Sequential([
    Conv2D(4, (2, 2), padding='same', activation='relu', input_shape=(input_w, input_h, 1)),
    MaxPool2D((2, 2)),
    Conv2D(8, (2, 2), padding='same', activation='relu'),
    MaxPool2D((2, 2))
])

# 学習をさせない
cnn_layers.trainable = False

# 後ろに全結合層をつなげる
model_cnn = Sequential()

model_cnn.add(cnn_layers)
model_cnn.add(Flatten())
model_cnn.add(Dense(10, activation='softmax'))

model_cnn.compile(
    loss=categorical_crossentropy, optimizer=Adam(), metrics=[categorical_accuracy]
)

print(model_cnn.summary())
# _________________________________________________________________
# Layer (type)                 Output Shape              Param #   
# =================================================================
# sequential_34 (Sequential)   (None, 7, 7, 8)           156       
# _________________________________________________________________
# flatten_23 (Flatten)         (None, 392)               0         
# _________________________________________________________________
# dense_54 (Dense)             (None, 10)                3930      
# =================================================================
# Total params: 4,086
# Trainable params: 3,930
# Non-trainable params: 156
# _________________________________________________________________

model_cnn.fit(
    X_train, y_train, batch_size=batch_size, epochs=epochs, 
    validation_data=(X_test, y_test), verbose=1
)
# Train on 60000 samples, validate on 10000 samples
# Epoch 1/10
# 60000/60000 [==============================] - 7s 116us/step - loss: 0.9706 - categorical_accuracy: 0.8011 - val_loss: 0.5502 - val_categorical_accuracy: 0.8767
# ...
# Epoch 10/10
# 60000/60000 [==============================] - 6s 105us/step - loss: 0.2198 - categorical_accuracy: 0.9390 - val_loss: 0.2128 - val_categorical_accuracy: 0.9421

ということで、全てのピクセルを全結合層でつなぐよりも、ランダムなCNNで特徴抽出したほうが、パラメータ数が減り (7850 -> 3930) 、精度も向上する (92.6 -> 94.2) という結果が得られました。

ちなみに、Conv2Dを除いてMaxPool2Dのみ (つまり縮約のみ) にしたバージョンでも試してみましたが、精度は83.1%にとどまりました。このことからも、畳み込み計算が特徴抽出器として優秀であることがわかります。

ガウス混合分布のパラメータをscikit-learnで推定する

scikit-learnでガウス混合分布のパラメータをさくっと推定する方法がありましたので、その備忘録です。

ガウス混合分布

ガウス混合分布は、複数のガウス分布を線形結合した分布で、以下式で表されます。


p(\vec{x})=\sum_i^N w_i N(\vec{x} | \mu_i, \sigma^2_i)

  • N: ガウス分布数 (ハイパパラメータ)
  •  w_i : ガウス分布の重み ( \sum_i^N w_i=1)

パラメータは  w_i, \mu_i, \sigma^2_i で、3×N個となります。

音声認識などでは、このガウス混合分布モデルと隠れマルコフモデルと組み合わせた手法が使われています。

scikit-learnのGaussianMixture

ガウス混合分布はEMアルゴリズムでパラメータを推定することになりますが、scikit-learnではガウス混合分布のパラメータ推定を行うツールとしてGaussianMixtureが提供されています。

irisデータセットの萼片長 (sepal length) を使って、GaussianMixtureによるパラメータ推定を実装していきます。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import seaborn as sns

from sklearn.mixture import GaussianMixture
from sklearn.datasets import load_iris

# irisデータセットのロード
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target

# 種類 (ラベル) によって、サンプル数を変えます
d1 = df[df['label'] == 0].sample(30)  # setosa
d2 = df[df['label'] == 1].sample(50)  # versicolor
d3 = df[df['label'] == 2].sample(40)  # virginica

# 萼片長のデータのみを使う
X = pd.concat([d1['sepal length (cm)'], d2['sepal length (cm)'], d3['sepal length (cm)']])
Y = pd.concat([d1['label'], d2['label'], d3['label']])

# ヒストグラム
plt.hist([X[Y==0], X[Y==1], X[Y==2]], bins=np.arange(X.min(), X.max(), 0.2), stacked=True, label=iris.target_names)
plt.legend()
plt.plot()

種類 (0: setosa, 1: versicolor, 2: virginica) によって萼片長の分布は異なっています。
足し合わさっているので、全体的には5.0, 5.6, 6.2付近をピークとした多峰性の分布となっていますが、いずれも混じりあっているため、境界によってきれいに分離できません。

f:id:ohke:20190608173834p:plain

それではGaussianMixtureで学習させます。

  • n_componentsは分布数で、3を設定
  • covariance_typeで分散の種類を選択でき、"spherical"とすると各分布は単一の分散になります
  • 推定されたパラメータは、weights_, means_, covariance_ にそれぞれ入ってます
# GaussianMixtureの学習
gmm = GaussianMixture(
    n_components=3,
    covariance_type='spherical'
).fit(
    np.array(X).reshape(-1, 1)  # 次元数2を入力とするため変形
)

# 重み
print(gmm.weights_)
# [0.309359   0.43752389 0.25311711]

# 期待値
print(gmm.means_)
# [[5.80722255]
#  [6.57885203]
#  [4.93215112]]

# 分散
print(gmm.covariances_)
# [0.10564111 0.32196481 0.06607687]

この3つの分布を描画します。

x = np.linspace(3, 9, 600)

gd1 = mlab.normpdf(x, gmm.means_[0, -1], np.sqrt(gmm.covariances_[0]))
gd2 = mlab.normpdf(x, gmm.means_[1, -1], np.sqrt(gmm.covariances_[1]))
gd3 = mlab.normpdf(x, gmm.means_[2, -1], np.sqrt(gmm.covariances_[2]))
    
plt.plot(x, gmm.weights_[0] * gd1, label='gd1')
plt.plot(x, gmm.weights_[1] * gd2, label='gd2')
plt.plot(x, gmm.weights_[2] * gd3, label='gd3')
plt.legend()
plt.show()

期待値4.9, 5.8, 6.6の3つの正規分布が描かれています。またgb1 (versicolor) とgb2 (virginica) は重なりも大きいことがわかります。

f:id:ohke:20190608181445p:plain

ある値xに対して、それがどの分布が占める割合が高いかをpredictメソッドで計算することができ、これにより (ソフト) クラスタリングができます。
この例の場合、71.7%の精度で正しく判定できました。

# 属する分布を予測
Y_predict = gmm.predict(np.array(X).reshape(-1, 1))
print(Y_predict)
# [2 2 0 2 2 2 2 2 2 2 2 2 2 2 0 0 2 0 2 2 2 0 2 2 2 2 2 2 2 2 0 0 2 1 0 1 0
#  0 0 0 0 2 0 0 1 0 2 1 0 1 0 1 0 1 0 0 0 1 0 0 1 2 0 0 0 1 1 0 0 1 1 0 0 0
#  0 0 1 1 2 1 1 1 0 1 1 1 1 1 0 1 0 0 0 1 1 1 1 1 0 2 1 1 1 1 1 1 1 1 0 1 1
#  1 1 1 1 1 1 1 1 1]

# ラベルを 0->2, 1->0, 2->1 へ置き換える
Y_new = Y.copy()
Y_new[Y==0] = 2
Y_new[Y==1] = 0
Y_new[Y==2] = 1

# 精度を計算
print(sum(Y_new == Y_predict) / len(Y_new))
# 0.7166666666666667

Sudachiベースの学習済みWord2Vecモデルを使う

お仕事でSudachiを使って形態素解析を行っているのですが、それと互換した単語埋め込みモデルが必要になりました。

Sudachiの開発元であるワークスアプリケーションズさんから、Sudachiで分かち書き・学習したWord2Vecモデルが提供されています。

大規模コーパスと複数粒度分割による日本語単語分散表現

モデルのダウンロード

上のURLから学習済みモデルをダウンロードします。

  • 20190314のバージョンで、語数3644628、次元数300となってます
  • 圧縮ファイルで5GB、解凍後で12GBくらいありますので注意してください
$ wget https://object-storage.tyo2.conoha.io/v1/nc_2520839e1f9641b08211a5c85243124a/word_vector/nwjc.sudachi_full_abc_w2v.20190314.tar.gz

$ tar -zxvf ./nwjc.sudachi_full_abc_w2v.20190314.tar.gz

$ ls -l nwjc_sudachi_full_abc_w2v/
total 12229544
-rw-rw-r-- 1 1008 1008       11420 Mar 20 17:28 LICENSE
-rw-rw-r-- 1 1008 1008 12523039651 Mar 20 12:49 nwjc.sudachi_full_abc_w2v.txt

$ head -n2 nwjc_sudachi_full_abc_w2v/nwjc.sudachi_full_abc_w2v.txt
3644628 300-0.08509623 -0.02170456 -0.00595318 ...

gensimで操作しますので、こちらもインストールしておきます。

$ pip install gensim

モデルのロードと確認

sudachi_full_abc_w2v.txt (テキストフォーマット) はKeyedVectorsでロードできます。上のheadコマンドで見たように語数3644628, 次元数300となっていることも確認できます。

# モデルのロード
from gensim.models import KeyedVectors
from gensim.test.utils import datapath

nwjc_model = KeyedVectors.load_word2vec_format(
    datapath('/tf/rest_natural_word_search/nwjc_sudachi_full_abc_w2v/nwjc.sudachi_full_abc_w2v.txt'),
    binary=False
)

# 語数, 次元数
print(len(nwjc_model.vocab), nwjc_model.vector_size)  # 3644628 300

こうなるとgensimの普通のモデルの同じように取り扱えますので、例えばmost_similarメソッドで類似した言葉を取り出すこともできます。

print(nwjc_model.most_similar('平成', topn=5))
# [('年度', 0.8092531561851501),
#  ('平成元年', 0.7409119009971619),
#  ('同年', 0.6042426824569702),
#  ('基本計画', 0.5917632579803467),
#  ('別紙', 0.590812087059021)]

print(nwjc_model.most_similar(positive=['兄弟', '女'], negative=['男'], topn=5))
# [('姉妹', 0.7687463164329529),
#  ('兄妹', 0.7484474182128906),
#  ('姉弟', 0.715774416923523),
#  ('双子', 0.642593264579773),
#  ('妹', 0.6402267217636108)]