終了済み: 「くずし字」識別チャレンジ

β版ProbSpaceコンペ第2弾!

賞金: 100,000 参加チーム数: 144 7ヶ月前に終了

学習データと試験データの「字母」のかたより

学習データと試験データの「字母」のかたより

1. はじめに

KMNISTをCNNモデルで評価したところ、CV=0.99程度に対してLB=0.971と出ました。

したがって、KMNISTの学習データと試験データにかたよりがあるのではないかと考えてデータを調べました。

その結果、ちょっと面白い評価ができましたので紹介します。

この検討には、ProbSpaceから提供されたkmnist-test-imgs.npz、kmnist-train-labels.npz、kmnist-train-imgs.npz以外に、人文学オープンデータ共同利用センターで公開されているkmnist-test-labels.npz(試験データの正解ラベル)も用いました。 コンペ期間中に外部データを参照することはルール違反ですが、コンペも数か月前に終わっていますので、問題ないと判断します。(というか、話題提供のために許してください…)

また、この検討はQiitaのTSNE Grid で MNIST を調べてみたを参考にしました。

2. くずし字とKMNISTについて

くずし字(変体仮名ともいう)には、現在の平仮名のもとになったものの他に様々な字母(もとになった漢字)が存在します。(「き」→「畿」,「起」,「支」、「す」→「寸」,「春」,「須」など) (変体仮名 五十音順一覧 )

同じ平仮名のくずし字に複数の字体があるのはこのためです。

KMNISTには、「お」,「き」,「す」,「つ」,「な」,「は」,「ま」,「や」,「れ」,「を」の十文字のくずし字が、学習データで60,000字(それぞれの字で6,000字)、試験データで10,000字(それぞれの字で1,000字)収められています。

3. 準備

まずは利用しているモジュールと画像データおよびラベルデータを読みます。

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
from matplotlib import offsetbox, patheffects
from mpl_toolkits.axes_grid1 import make_axes_locatable
from scipy.spatial.distance import cdist
from sklearn.datasets import fetch_openml
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.utils import shuffle
from sklearn.preprocessing import minmax_scale
from lapjv import lapjv
from PIL import Image

rc = {
  'font.family': ['sans-serif'],
  'font.sans-serif': ['Open Sans', 'Arial Unicode MS'],
  'font.size': 12,
  'figure.figsize': (8, 6),
  'grid.linewidth': 0.5,
  'legend.fontsize': 10,
  'legend.frameon': True,
  'legend.framealpha': 0.6,
  'legend.handletextpad': 0.2,
  'lines.linewidth': 1,
  'axes.facecolor': '#fafafa',
  'axes.labelsize': 10,
  'axes.titlesize': 14,
  'axes.linewidth': 0.5,
  'xtick.labelsize': 10,
  'xtick.minor.visible': True,
  'ytick.labelsize': 10,
  'figure.titlesize': 14
}
sns.set('notebook', 'whitegrid', rc=rc)

def colorize(d, color, alpha=1.0):
  rgb = np.dstack((d,d,d)) * color
  return np.dstack((rgb, d * alpha)).astype(np.uint8)

#colors = sns.color_palette('tab10')
colors = sns.color_palette(n_colors=24)
# Load the data(kmnist-test-labels.npzは外部データ)
Train_imgs = np.load("./data/kmnist-train-imgs.npz")['arr_0']
target = np.load("./data/kmnist-train-labels.npz")['arr_0']
Test_imgs = np.load("./data/kmnist-test-imgs.npz")['arr_0']
Test_target = np.load("./data/kmnist-test-labels.npz")['arr_0']
# Train_imgsとTest_imgsを一次元データにする
data = Train_imgs.reshape(-1,784)
Test_data = Test_imgs.reshape(-1,784)

試験データのラベルを+10して、学習データのラベルと区別できるようにしたうえで、試験データと学習データを結合します。

# テストデータの正解ラベルを+10する。
Test_target += 10

data = np.concatenate([data, Test_data])
target = np.concatenate([target, Test_target])

ラベルと仮名の対応を設定します。

学習データと試験データが区別できるようにします。

# 文字対応リストと日本語対応フォントの設定
labels = { 0: 'お(train)', 1: 'き(train)', 2: 'す(train)', 3: 'つ(train)', 4: 'な(train)', 
          5: 'は(train)', 6: 'ま(train)', 7: 'や(train)', 8 : 'れ(train)', 9 : 'を(train)',
           10: 'お(test)', 11: 'き(test)', 12: 'す(test)', 13: 'つ(test)', 14: 'な(test)', 
          15: 'は(test)', 16: 'ま(test)', 17: 'や(test)', 18 : 'れ(test)', 19 : 'を(test)'}
# plt.rcParams['font.family'] = 'IPAPGothic'
plt.rcParams['font.family'] = 'Malgun Gothic'

4. くずし字の画像データ

くずし字の画像を30個ずつ表示します。

上の10が学習データのもので、下の10が試験データのものです。

よく見ると、ほとんどの文字で、異なる字母を持つものが混在しています。

#data, target = fetch_openml('mnist_784', version=1, return_X_y=True)
#data = data.astype('float32')
#target = target.astype('uint8')

fig, ax = plt.subplots(figsize=(16,8))

size = 28
dim = (20,30)

img = np.zeros((size * dim[0], size * dim[1], 4),dtype='uint8')

for num in range(20):
    for d, t, i in zip(data[target == num], target[target == num], range(dim[1])):
      ix = num
      iy = i
      img[ix*size:(ix+1)*size,iy*size:(iy+1)*size,:] = colorize(d.reshape(size,size), colors[t], 0.9)

ax.imshow(img)
ax.set_axis_off()
plt.show()

5. t-SNEで2次元マッピング

次は、学習データのうち24×24=576のデータをランダムに抽出し、t-SNE を利用して、次元削減し、平面にマッピングします。

t-SNEの理論は全く理解できていませんが、近い特徴を持つものを近くに、遠い特徴を持つものを遠くにマッピングしてくれる便利なツールと思っています。

size = 24
n = size * size
x_data, y_data = shuffle(data, target, n_samples=n)
x_pca = PCA(n_components=50).fit_transform(x_data/255)
embeddings = TSNE(perplexity=50, random_state=24680, verbose=2).fit_transform(x_pca)
embeddings = minmax_scale(embeddings)

fig, ax = plt.subplots(figsize=(12,12))

for i in range(10):
  ax.scatter(embeddings[y_data==i,0],embeddings[y_data==i,1],cmap='tab10',marker='o',alpha=0.7,label=labels[i])
  x_,y_ = np.median(embeddings[y_data==i,:],axis=0)
  txt = ax.text(x_,y_,labels[i],fontsize=30)
  txt.set_path_effects([
    patheffects.Stroke(linewidth=7, foreground="w"),
    patheffects.Normal()
  ])

ax.xaxis.set_ticks([])
ax.yaxis.set_ticks([])
ax.legend(loc='lower right')

plt.show()
[t-SNE] Computing 151 nearest neighbors...
[t-SNE] Indexed 576 samples in 0.005s...
[t-SNE] Computed neighbors for 576 samples in 0.034s...
[t-SNE] Computed conditional probabilities for sample 576 / 576
[t-SNE] Mean sigma: 3.304848
[t-SNE] Computed conditional probabilities in 0.031s
[t-SNE] Iteration 50: error = 66.0491257, gradient norm = 0.4408489 (50 iterations in 0.281s)
[t-SNE] Iteration 100: error = 66.5490036, gradient norm = 0.4279355 (50 iterations in 0.227s)
[t-SNE] Iteration 150: error = 66.1810837, gradient norm = 0.4408225 (50 iterations in 0.232s)
[t-SNE] Iteration 200: error = 66.5024948, gradient norm = 0.4372374 (50 iterations in 0.249s)
[t-SNE] Iteration 250: error = 67.7577744, gradient norm = 0.4174628 (50 iterations in 0.248s)
[t-SNE] KL divergence after 250 iterations with early exaggeration: 67.757774
[t-SNE] Iteration 300: error = 1.0015848, gradient norm = 0.0030262 (50 iterations in 0.203s)
[t-SNE] Iteration 350: error = 0.9373937, gradient norm = 0.0009318 (50 iterations in 0.157s)
[t-SNE] Iteration 400: error = 0.9023022, gradient norm = 0.0010213 (50 iterations in 0.169s)
[t-SNE] Iteration 450: error = 0.8871266, gradient norm = 0.0003354 (50 iterations in 0.172s)
[t-SNE] Iteration 500: error = 0.8833435, gradient norm = 0.0002266 (50 iterations in 0.160s)
[t-SNE] Iteration 550: error = 0.8798280, gradient norm = 0.0001704 (50 iterations in 0.172s)
[t-SNE] Iteration 600: error = 0.8746018, gradient norm = 0.0003532 (50 iterations in 0.161s)
[t-SNE] Iteration 650: error = 0.8552826, gradient norm = 0.0007609 (50 iterations in 0.156s)
[t-SNE] Iteration 700: error = 0.8477432, gradient norm = 0.0002363 (50 iterations in 0.176s)
[t-SNE] Iteration 750: error = 0.8456022, gradient norm = 0.0001718 (50 iterations in 0.156s)
[t-SNE] Iteration 800: error = 0.8452249, gradient norm = 0.0002175 (50 iterations in 0.163s)
[t-SNE] Iteration 850: error = 0.8443690, gradient norm = 0.0001080 (50 iterations in 0.172s)
[t-SNE] Iteration 900: error = 0.8439267, gradient norm = 0.0000835 (50 iterations in 0.159s)
[t-SNE] Iteration 950: error = 0.8437628, gradient norm = 0.0000937 (50 iterations in 0.172s)
[t-SNE] Iteration 1000: error = 0.8437957, gradient norm = 0.0000555 (50 iterations in 0.162s)
[t-SNE] KL divergence after 1000 iterations: 0.843796