kaggle の Digit Recognizer に挑戦する (3)

前回、kaggle の Digit Recognizer コンペティションで、畳み込みニューラルネットワークを利用しました。このコンペティションの Code で最も投票数を集めているのが Introduction to CNN Keras - 0.997 (top 6%) です。この中に、データを拡張して学習する方法が紹介されていますので、今回はこれを試していきたいと思います。

データ拡張

今回試すデータの拡張とは具体的に言うと、元々の手書き文字画像から、角度を少し変えてみたり、上下左右に少し動かしてみたり、大きさを少し変えてみたりした画像を生成して、それらの画像も学習に利用する、というものです。これで大幅に精度が上がったと書かれています。これらの画像の生成には、keras の ImageDataGenerator が利用できるので、それを使っていきます。

まずは、畳み込みニューラルネットワークコンペティションのことは一旦忘れて、単純に ImageDataGenerator を使って画像を生成してみます。データの下準備は、前回までとほぼ同じです。

from keras.models import Sequential
from keras.layers import Dense, Conv2D, MaxPooling2D, Flatten
from keras.utils import to_categorical
from keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
train = np.loadtxt('/root/practice/digit/csv/train.csv', skiprows=1, delimiter=',')
x_test = np.loadtxt('/root/practice/digit/csv/test.csv', skiprows=1, delimiter=',')
# print(train.shape)
# print(x_test.shape)

# 1列目と2〜785列目を分ける
y_train, x_train = np.split(train, [1], 1)
# print(x_train.shape)
# print(y_train.shape)

# 形状を枚数 × 28 × 28 × 1 に変形する
x_train = x_train.reshape(42000, 28, 28, 1)
x_test = x_test.reshape(28000, 28, 28, 1)
# print(x_train.shape)
# print(x_test.shape)

# 1枚分を取り出し表示させてみる
plt.imshow(x_train[3])

# ラベル確認
# print(y_train[3])

ここから ImageDataGenerator を使います。 今回はこの手書き数字「4」の画像を元画像として、これをコピーして3枚にし、これを使っていきます。

sample_images = np.array([x_train[3].copy(), x_train[3].copy(), x_train[3].copy()])

# -90 〜 90の範囲でランダムに回転
datagen = ImageDataGenerator(rotation_range=90)

# 3枚の画像を生成するので batch_size = 3  戻り値はイテレータ
g = datagen.flow(sample_images, batch_size=3)
batches = g.next()

#(枚数, 縦サイズ, 横サイズ, チャンネル数) --> (3, 28, 28, 1) になる
# print(batches.shape)

# 画像表示
plt.subplot(1, 3, 1)
plt.imshow(batches[0])
plt.subplot(1, 3, 2)
plt.imshow(batches[1])
plt.subplot(1, 3, 3)
plt.imshow(batches[2])

これで実際に画像を表示させてみると、「4」が回転している画像が3枚できたことがわかります。

学習

では、この ImageDataGenerator で、様々な画像データを生成して、学習していきます。CSVからデータを読み込み、訓練データと教師データを分ける処理などは上記と同じで、データの下準備と、ニューラルネットワークの構築部分は前回と同じです。

# 0 - 1 の間の数値にする
x_train  = x_train.astype('float32')
x_test   = x_test.astype('float32')
x_train /= 255
x_test  /= 255

# one-hot 表現にする
y_train = to_categorical(y_train, 10)

# モデルは前回の設定と同じ
model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(4, 4), activation='relu', input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=64, kernel_size=(4, 4), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

それでは、ここから画像データを拡張していきます。

epochs = 30
batch_size = 86

datagen = ImageDataGenerator(
    rotation_range=10,      # ランダムに10度回転する
    zoom_range=0.1,         # ランダムに10%ズームする
    width_shift_range=0.1,  # ランダムに横方向に10%ずらす
    height_shift_range=0.1  # ランダムに縦方向に10%ずらす
)

# 第一引数は画像データ、第二引数はラベル
generator = datagen.flow(x_train, y_train, batch_size=batch_size)
# 第一引数はデータを生成する generator、引数 steps_per_epoch は1エポック当たり generator を呼び出す回数
history = model.fit_generator(
    generator,
    epochs=epochs,
    steps_per_epoch=(x_train.shape[0] // batch_size),
)

generator が生成するデータで学習をするときは、fit_generator メソッドを利用します。引数 steps_per_epoch は画像データ数をバッチサイズで割ったものを指定しています。これで1エポック当たりで、元々の画像データ数と同じ数のデータが生成されます。

学習が終わったら、誤差と精度を求めます。

loss, accuracy = model.evaluate(x_train, y_train)
print(loss)
print(accuracy)

前回(一つ前のブログ記事の (1) + (4) + (5) + (6))の結果と比較します。

loss accuracy
前回 0.008217111229896545 0.9972618818283081
今回 0.011472832411527634 0.9966190457344055

前回と比較して誤差・精度共に良い結果になりませんでした。
この学習後のニューラルネットワークで予測を行い、その結果を kaggle に提出してみます。

predicts = model.predict(x_test)

# 最も値の大きいものが予測された値なので、それを取り出す
predicts_label = np.argmax(predicts, axis=1)

# CSVを出力するためのデータを生成
df = pd.DataFrame({
    'imageId': list(range(1, len(predicts)+ 1)),
    'Label': predicts_label
})

# CSV出力
df.to_csv("/root/practice/digit/csv/predictions.csv", index=False)

すると、なんとスコアが更新されました!!!!!!!!!

訓練データに対しては前回の結果を上回ることはできませんでしたが、テストデータに対しては良い結果を得ることができたようです。

kaggle の Digit Recognizer に挑戦する (2)

前回 kaggle の Digit Recognizer というコンペティションに挑戦しましたが、今回は、ニューラルネットワークの構成を変えて、畳み込みニューラルネットワークで学習をしたいと思います。畳み込みニューラルネットワークについては、私が理解するのに参考にした動画やサイトを、この記事の最後にリンクしておきましたので、そちらを参照してください。

データ

データの準備は前回とほぼ同じなので、記載を適宜割愛しますが、畳み込みニューラルネットワークへの入力を 28 * 28 * 1 にしないといけないので、その部分を調整していきます。

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Conv2D, MaxPooling2D, Flatten
from keras.utils import to_categorical
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
# データ読み込み
train = np.loadtxt('/root/practice/digit/csv/train.csv', skiprows=1, delimiter=',')
x_test = np.loadtxt('/root/practice/digit/csv/test.csv', skiprows=1, delimiter=',')
# print(train.shape)
# print(x_test.shape)

# 1列目と2〜785列目を分ける
y_train, x_train = np.split(train, [1], 1)
# print(x_train.shape)
# print(y_train.shape)

# 形状を 枚数 × 28 × 28 × 1 に変形する
x_train = x_train.reshape(42000, 28, 28, 1)
x_test = x_test.reshape(28000, 28, 28, 1)
# print(x_train.shape)
# print(x_test.shape)

# 1枚分を取り出し表示させてみる
# plt.imshow(x_train[0])

# ラベル確認
# print(y_train[0])

# 0 - 1 の間の数値にする
x_train  = x_train.astype('float32')
x_test   = x_test.astype('float32')
x_train /= 255
x_test  /= 255

# one-hot 表現にする
y_train = to_categorical(y_train, 10)
# print(y_train[0])

畳み込みニューラルネットワーク構築

それでは、畳み込みニューラルネットワークを構築していきます。

model = Sequential()
# 畳み込み層
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
# プーリング層
model.add(MaxPooling2D(pool_size=(2, 2)))
# 出力を1次元にする
model.add(Flatten())
# 出力層
model.add(Dense(10, activation='softmax'))

model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 26, 26, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 13, 13, 32)       0         
 )                                                               
                                                                 
 flatten (Flatten)           (None, 5408)              0         
                                                                 
 dense (Dense)               (None, 10)                54090     
                                                                 
=================================================================
Total params: 54,410
Trainable params: 54,410
Non-trainable params: 0
_________________________________________________________________

Conv2Dが畳み込み層になります。各引数の意味は下記の通りです。

  • filters: 出力空間の次元=出力フィルタの数
  • kernel_size: 2次元畳み込みウィンドウの大きさ
  • activation: 活性化関数
  • input_shape: 入力次元

MaxPooling2Dはプーリング層になります。引数 pool_size はプーリングするウィンドウの大きさです。

それでは学習していきます。

history = model.fit(x_train, y_train, epochs=5)

結果をグラフで表示します。

sns.lineplot(data=history.history["loss"])

sns.lineplot(data=history.history["accuracy"])

学習が進むにつれて、誤差が減少し、精度が増加していくことがわかります。具体的な誤差と精度の数値も求めておきます(結果は後述)。

loss, accuracy = model.evaluate(x_train, y_train)
print(loss)
print(accuracy)

設定を変える

上で紹介した畳み込みニューラルネットワークの構成をベースとして、いろいろと一つずつパラメータや構成を変えて誤差と精度がどう変わるのか試してみます。変えたパラメータや構成以外はベースと同じです。今回は下記のようにパラメータ・構成を変えてみました。

(1) Conv2D の kernel_size を (4, 4) にする
(2) Conv2D の kernel_size を (2, 2) にする
(3) MaxPooling2D の pool_size を (3, 3) にする
(4) epochs を 10 にする
(5) 全結合層を追加する
(6) Conv2D, MaxPooling2D の層を追加する

「(5) 全結合層を追加する」は、具体的に下記のように設定しています。

model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64, activation='relu'))  # この行を追加
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

「(6) Conv2D, MaxPooling2D の層を追加する」は、具体的に下記のように設定しています。

model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu'))  # この行を追加
model.add(MaxPooling2D(pool_size=(2, 2)))  # この行を追加
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

それぞれの結果は下記の通りになりました。

loss accuracy
ベース 0.0354999378323555 0.9896904826164246
(1) Conv2D の kernel_size を (4, 4) にする 0.03126463294029236 0.9907143115997314
(2) Conv2D の kernel_size を (2, 2) にする 0.04609178006649017 0.9873571395874023
(3) MaxPooling2D の pool_size を (3, 3) にする 0.05607537552714348 0.9817143082618713
(4) epochs を 10 にする 0.015390428714454174 0.9960476160049438
(5) 全結合層を追加する 0.012460934929549694 0.9962618947029114
(6) Conv2D, MaxPooling2D の層を追加する 0.016651742160320282 0.9953095316886902

まず、畳み込み層の畳み込みウィンドウのサイズですが、学習する画像のサイズが28 × 28だと、3 × 3 よりも少し大きくした 4 × 4 の方が良いようです。学習回数は少なかったようで、epoch=10にしたら、良い結果が出ました。全結合層、Conv2D・MaxPooling2D の層を増やした方が良い結果が出ています。

この結果を踏まえて、ベースの設定から下記の設定を加えたもので再度学習し直してみます。

(1) Conv2D の kernel_size を (4, 4) にする
(4) epochs を 10 にする
(5) 全結合層を追加する
(6) Conv2D, MaxPooling2D の層を追加する

具体的には下記のような設定になります。

model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(4, 4), activation='relu', input_shape=(28, 28, 1)))  # kernel_size を (4, 4) にする
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=64, kernel_size=(4, 4), activation='relu'))  # この行を追加  kernel_size を (4, 4) にする
model.add(MaxPooling2D(pool_size=(2, 2)))  # この行を追加
model.add(Flatten())
model.add(Dense(64, activation='relu'))  # この行を追加
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

history = model.fit(x_train, y_train, epochs=10)  # epochs を 10 にする

上記の設定の結果は下記のようになりました。今までで一番良い結果になりましたね。

loss accuracy
(1) + (4) + (5) + (6) 0.008217111229896545 0.9972618818283081

予測

それでは、上記構成で予測します。前回と同様のソースです。

predicts = model.predict(x_test)

# 最も値の大きいものが予測された値なので、それを取り出す
predicts_label = np.argmax(predicts, axis=1)

# CSVを出力するためのデータを生成
df = pd.DataFrame({
    'imageId': list(range(1, len(predicts)+ 1)),
    'Label': predicts_label
})

# CSV出力
df.to_csv("/root/practice/digit/csv/predictions.csv", index=False)

CSVが出力されたので kaggle に提出します。

おおおおおおおおおおおお!前回よりもスコアとランキングが上がりました!!

参考にしたサイト

kaggle の Digit Recognizer に挑戦する(1)

今回は kaggle の Digit Recognizer というコンペティションに挑戦したいと思います。このコンペティションは簡単に言うと画像認識で、0〜9の数字を手で書いた画像があるので、それが何の数字なのかを予測する、というものです。なお、開発は Jupyter Notebook を利用し、環境は Docker tensorflow/tensorflow:latest-jupyter イメージで構築しています。

MNIST のデータ

実は MNIST にも同じようなデータがあり、こちらの方が理解しやすいので、まずはこちらを説明します。手書きの数字画像は縦28×横28ピクセルの画像で、各ピクセルには色の濃淡を表す0〜255までの整数が入っています。この数値は、0だと真っ白で、255だと真っ黒を表します。MNIST のデータは解説しているサイトがたくさんありますので、例えばこちらのサイトを参照してもらえば、何を言っているのかすぐに理解してもらえると思います。

では、実際にダウンロードして確かめてみます。

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.utils import to_categorical
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
(xm_train, ym_train), (xm_test, ym_test) = mnist.load_data()
print(xm_train.shape)
(60000, 28, 28)

60000 × 28 × 28 の numpy.ndarray であることが確認できます。縦28×横28ピクセルの画像データが60000枚あるということです。matplotlib の imshow() を使うと、画像で表示することができます。

plt.imshow(xm_train[0])

対応するラベル y_train を確認します。

print(ym_train[0])
5

手書き数字画像と、ラベルが一致することが確認できました。

kaggle のデータ

kaggle のデータはMNISTのデータとはちょっと違います。実際にデータをダウンロードし、データを読み込んで中身を確認します。

train = np.loadtxt('/root/practice/digit/csv/train.csv', skiprows=1, delimiter=',')
x_test = np.loadtxt('/root/practice/digit/csv/test.csv', skiprows=1, delimiter=',')
print(train.shape)
print(x_test.shape)
(42000, 785)
(28000, 784)

訓練データは 42000 × 785 になっています。42000は手書き数字画像の枚数です。785は、1列目はラベル、即ち手書き画像の数値が何かを表したもので、2〜785列目は縦28×横28ピクセルの画像データを横一列に並べたものものです。28 × 28 = 784 なので数も合いますね。テストデータは、訓練データと比較して、ラベルの列が無いものになります。

実際にデータを加工して確かめてみます。

# 1列目と2〜785列目を分ける
y_train, x_train = np.split(train, [1], 1)
print(x_train.shape)
print(y_train.shape)
(42000, 784)
(42000, 1)
# 1枚分を取り出し 28 * 28 の形にして表示させてみる
plt.imshow(x_train[0].reshape(28, 28))

# ラベル確認
print(y_train[0])
[1.]

画像が表示され、ラベルと一致することが確認できました。

データ準備

学習をする前にデータの下準備をしていきます。

# 0 - 1 の間の数値にする
x_train  = x_train.astype('float32')
x_test   = x_test.astype('float32')
x_train /= 255
x_test  /= 255
# one-hot 表現にする
y_train = to_categorical(y_train, 10)
print(y_train[0])
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]

ラベルの方を one-hot 表現にしています。訓練データの最初の数値は1でしたから、(0から数えて)1を表すところが1になっていて、後は全て0になっていることが確認できます。

学習

それでは、ニューラルネットワークを構築して学習していきます。

# ニューラルネットワーク構築
model = Sequential()
# [中間層] ユニット数:12, 活性化関数:relu, 入力次元数:784
model.add(Dense(units=12, activation='relu', input_dim=784))
# [出力層] ユニット数:10, 活性化関数:ソフトマックス
model.add(Dense(10, activation='softmax'))
# 損失関数:categorical_crossentropy, 最適化アルゴリズム:rmsprop
model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense (Dense)               (None, 12)                9420      
                                                                 
 dense_1 (Dense)             (None, 10)                130       
                                                                 
=================================================================
Total params: 9,550
Trainable params: 9,550
Non-trainable params: 0
_________________________________________________________________

各層の設定はソース内のコメントとして書きましたので、参照してください。

それでは学習していきます。

# バッチサイズ:128, 反復数:20
history = model.fit(x_train, y_train, batch_size=128, epochs=20, verbose=1)

結果をグラフで表示します。

sns.lineplot(data=history.history["loss"])

sns.lineplot(data=history.history["accuracy"])

学習が進むにつれ、誤差が減少し、精度が増加していくことがわかります。

設定を変える

最適化アルゴリズムを adam に変えて試してみます。具体的には下記の optimizer 引数を adam に変更します。他の設定は全く同じです。

# 損失関数:categorical_crossentropy, 最適化アルゴリズム:adam
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

誤差、精度ともに少し改善したようです。

中間層のユニット数を128に増やしてみます。最適化アルゴリズムは adam を利用し、その他設定は同じです。具体的な修正箇所は下記です。

# [中間層] ユニット数:128, 活性化関数:relu, 入力次元数:784
model.add(Dense(units=128, activation='relu', input_dim=784))

誤差、精度ともに大幅に改善されたようです。 evaluate メソッドで具体的な数値も比較してみます。

loss, accuracy = model.evaluate(x_train, y_train, verbose=1)
loss accuracy
rmsprop 0.1851324439048767 0.9472380876541138
adam 0.1607203185558319 0.9545952677726746
adam + Unit数128 0.00736998999491334 0.9989285469055176

数値を見ても改善されているのが確認できます。

予測

では、 adam + 中間層ユニット数128 の設定で、テストデータを予測したいと思います。

predicts = model.predict(x_test)
# 0 - 9 の10列のうち、最も値の大きいものが予測された値なので、それを取り出す
predicts_label = np.argmax(predicts, axis=1)
# CSVを出力するためのデータを生成
df = pd.DataFrame({
    'imageId': list(range(1, len(predicts)+ 1)),
    'Label': predicts_label
})
print(df.tail())
imageId Label
27995 27996 9
27996 27997 7
27997 27998 3
27998 27999 9
27999 28000 2
# CSV出力
df.to_csv("/root/practice/digit/csv/predictions.csv", index=False)

無事にCSVが出力されたので、kaggle に提出します。

おおおおおおおおおおお!これも下から数えた方が早いですが、自分の名前が Leaderboard に刻まれました!!

参考にしたサイト

Kaggle のタイタニック問題をやってみる(2)

前回タイタニック問題のデータの中身を調査しましたので、今回はそのデータを元に学習し予測していきたいと思います。

訓練データ準備

今回使いたいデータの項目は、Pclass(チケットクラス)と年齢と性別です。まずは、年齢について何とかします。欠損値がたくさんあったので、これを何とかしないといけません。欠損値を何か適当なそれっぽいデータで埋めようと思うので、Pclass(チケットクラス)毎に年齢の平均を出してみます。

import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.model_selection import train_test_split
from keras.layers import Dense
from keras.models import Sequential
%matplotlib inline
df = pd.read_csv("/root/practice/titanic/csv/train.csv")
df[["Pclass", "Age"]].dropna().groupby(["Pclass"]).mean()
Pclass Age
1 38.233441
2 29.877630
3 25.140620

得られたPclass(チケットクラス)毎の年齢の平均値で、欠損値を埋めます。

# Age 欠損値を埋める
def fill_age(value):
    if not np.isnan(value[1]):
        return value[1]

    pclass_age_map = {
        1: 38,
        2: 30,
        3: 25
    }
    return pclass_age_map.get(value[0], 0)

df["Age"] = df[["Pclass", "Age"]].apply(fill_age, axis=1)

続いて、性別を0,1の値にして、学習に適した形にします。

# 性別を0, 1にする
sex = pd.get_dummies(df["Sex"], drop_first=True, dtype=int)
# 性別を0,1にしたデータを結合しておく --> male という列が最後にできている形になる 
df = pd.concat([df, sex], axis=1)

この時点でこのようなデータになっています。

df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked male
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 1
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Thayer) female 38.0 1 0 PC 17599 71.2833 C85 C 0
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 0
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S 0
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S 1

今回は、学習に利用する項目としてPclass(チケットクラス)と性別と年齢のみを対象にしたいので、必要な列を除いて(性別の元データ列も含めて)削除します。

# 不要カラム削除
df.drop(["Name", "Sex", "SibSp", "Parch", "Ticket", "Fare", "Cabin", "Embarked"], axis=1, inplace=True)

そうすると、これだけスッキリしたデータになりました。

df.head()
PassengerId Survived Pclass Age male
0 1 0 3 22.0 1
1 2 1 1 38.0 0
2 3 1 3 26.0 0
3 4 1 1 35.0 0
4 5 0 3 35.0 1

このデータで訓練データを作成します。作成には sklearn の train_test_splitを利用します。

X_train, X_test, y_train, y_test = train_test_split(
    df.drop(["Survived"], axis=1),
    df["Survived"],
    test_size=0.10,
    random_state=101
)

訓練データの元となるデータは、データから Survived 列を除いたもの(train_test_splitの第一引数)、教師データの元となるデータは Survived 列のデータ(train_test_splitの第二引数)になります。これで訓練データが作成できました。X_train, y_train の中身を確認しておきます。

X_train.head()
PassengerId Pclass Age male
825 826 3 25.0 1
8 9 3 27.0 0
689 690 1 15.0 0
513 514 1 54.0 0
729 730 3 25.0 0
y_train.head()
Survived
825 0
8 1
689 1
513 1
729 0

学習

データの下準備はできたので、学習していきます。まずはニューラルネットワークを構築します。

model = Sequential()

# [中間層] ユニット数: 32, 重み初期化方法: uniform, 活性化関数: reru, 入力: 4項目
model.add(Dense(units=32, kernel_initializer='uniform', activation='relu', input_dim=4))
model.add(Dense(units=32, kernel_initializer='uniform', activation='relu'))

# [出力層] ユニット(出力)数: 1, 重み初期化方法: uniform, 活性化関数: シグモイド
model.add(Dense(units=1, kernel_initializer='uniform', activation='sigmoid'))

# 最適化アルゴリズム: adam, 目的(損失)関数: 平均二乗誤差
model.compile(optimizer='adam', loss='mean_squared_error', metrics=['accuracy'])

三層のニューラルネットワークを構築しました。各層の詳細な設定はソース内のコメントを参照してください。

それでは学習していきます。

# バッチサイズ: 32, 反復数: 300
history = model.fit(X_train, y_train, batch_size=32, epochs=300, verbose=1)

学習結果を折れ線グラフで表示させてみます。

sns.lineplot(data=history.history["loss"])

sns.lineplot(data=history.history["accuracy"])

学習を重ねる毎に誤差が減少し、精度が増加していくことがわかります。

テストデータ準備

学習ができたので、テストデータの予測していきます。 前回のブログでも書いた通り、訓練データから「生存フラグ」項目が無いものがテストデータになります。 データの準備は訓練データと同じなので、同じ手順でデータを加工していきます。

df_test = pd.read_csv("/root/practice/titanic/csv/test.csv")
df_test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  418 non-null    int64  
 1   Pclass       418 non-null    int64  
 2   Name         418 non-null    object 
 3   Sex          418 non-null    object 
 4   Age          332 non-null    float64
 5   SibSp        418 non-null    int64  
 6   Parch        418 non-null    int64  
 7   Ticket       418 non-null    object 
 8   Fare         417 non-null    float64
 9   Cabin        91 non-null     object 
 10  Embarked     418 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
df_test.isnull().sum().sort_values(ascending=False)
data
Cabin 327
Age 86
Fare 1
PassengerId 0
Pclass 0
Name 0
Sex 0
SibSp 0
Parch 0
Ticket 0
Embarked 0

データ数は418行、テストデータの年齢の項目にも欠損値が多数あるようです。

# Age の欠損値を埋める
df_test["Age"] = df_test[["Pclass", "Age"]].apply(fill_age, axis=1)
# 性別を 0, 1 にする
sex = pd.get_dummies(df_test["Sex"], drop_first=True, dtype=int)
df_test = pd.concat([df_test, sex], axis=1)
df_test.head()
PassengerId Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked male
0 892 3 "Kelly, Mr. James" male 34.5 0 0 330911 7.8292 NaN Q 1
1 893 3 "Wilkes, Mrs. James (Ellen Needs)" female 47.0 1 0 363272 7.0000 NaN S 0
2 894 2 "Myles, Mr. Thomas Francis" male 62.0 0 0 240276 9.6875 NaN Q 1
3 895 3 "Wirz, Mr. Albert" male 27.0 0 0 315154 8.6625 NaN S 1
4 896 3 "Hirvonen, Mrs. Alexander (Helga E Lindqvist)" female 22.0 1 1 3101298 12.2875 NaN S 0
# 不要カラム削除
df_test.drop(["Name", "Sex", "SibSp", "Parch", "Ticket", "Fare", "Cabin", "Embarked"], axis=1, inplace=True)
df_test.head()
PassengerId Pclass Age male
0 892 3 34.5 1
1 893 3 47.0 0
2 894 2 62.0 1
3 895 3 27.0 1
4 896 3 22.0 0

予測

テストデータの準備ができたので、いよいよ予測していきたいと思います。

# 学習済みのニューラルネットワークで予測
test_predicts = model.predict(df_test)
test_predicts = [ 1 if y >= 0.5 else 0 for y in test_predicts]

出力された値(予測)を0,1にするところまでできたので、予め決められた提出するファイルのフォーマットにする準備をします。

# 提出用のCSVを出力する準備
df_test_result = pd.DataFrame({
    'PassengerId': df_test['PassengerId'],
    'Survived': test_predicts
})
df_test_result.head()
PassengerId Survived
0 892 0
1 893 1
2 894 0
3 895 0
4 896 1

後はCSVに出力するだけです。

df_test_result.to_csv("/root/practice/titanic/csv/predictions.csv", index=False)

提出

予測結果をCSVに出力することができたので、提出してみます。 Titanic - Machine Learning from Disasterのページに行き、「Submit Predictions」ボタンを押下して、CSVファイルをアップロードします。 エラーが無ければ、アップロードに成功した旨の通知が来るので、Leaderboardで自分の順位を確認します。

おおおおおおお!圧倒的に下から数えた方が早い順位ですが、Leaderboardに自分の名前が刻まれました!

Kaggle のタイタニック問題をやってみる(1)

今までGoの記事を書いてきましたが、一旦お休みして、今回は kaggle の Titanic - Machine Learning from Disaster に挑戦しようと思います。挑戦とはいえ、初心者ですので、下記を Code を参考にしながら進めていきたいと思います。

まずは、データの中身を見ていきます。

データ

このタイタニック問題のデータの中身については、いろいろなサイトで解説されていますので、横着しますが、詳細はこちらのサイトを参照してもらえればと思います。データの項目として、乗客の名前やチケットクラス、年齢、性別などのデータと生存フラグがあります。訓練データにはこれら全ての項目があり、テストデータはこれらの項目の中から生存フラグが無いデータになります。

生存に大きく関与した項目として考えられるのは「年齢」「性別」「チケットクラス」が挙げられると思います。今回はシンプルに考えるためにも、この3項目に注目して、データの中身を見ていきたいと思います。その前にまずは、データ全体の概要を見ていきます。

データ概要

import numpy as np
import pandas as pd
import seaborn as sns
%matplotlib inline

df = pd.read_csv("/root/practice/titanic/csv/train.csv")

df.info()
df.isnull().sum().sort_values(ascending=False)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
Cabin          687
Age            177
Embarked         2
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
SibSp            0
Parch            0
Ticket           0
Fare             0
dtype: int64

データ数は891行、Cabin(客室番号)とAge(年齢)、Embarked(出港地)に欠損値があるようです。

チケットクラス

Pclass(チケットクラス)についてグラフにしてみます。

sns.set_style("whitegrid")
sns.countplot(x="Pclass", data=df, hue="Survived")

Pclassの数値の意味ですが下記の通りで、上のクラスの人ほど生存率が高いことがわかります。

  • 1:上層クラス
  • 2:中級クラス
  • 3:下層クラス

性別

性別についてグラフにします。

sns.set_style("whitegrid")
sns.countplot(x="Sex", data=df, hue="Survived")

女性の方が生存率が高いことがわかります。

年齢

年齢についてグラフにします。

age_survived = df.query("Survived == 1 and Age > 0")
age_not_survived = df.query("Survived == 0 and Age > 0")
sns.histplot(x="Age", data=age_survived, bins=30, kde=True, color="green", alpha=0.5)
sns.histplot(x="Age", data=age_not_survived, bins=30, kde=True, color="red", alpha=0.5)

10代以下の人の生存率が高いことがわかります。30代の生存者数は多いようですが、死亡者数も多いので生存率としては高くないようです。

その他要素

その他の要素についてもグラフにします。

兄弟/配偶者の数

sns.set_style("whitegrid")
sns.countplot(x="SibSp", data=df, hue="Survived")

親/子供の数

sns.set_style("whitegrid")
sns.countplot(x="Parch", data=df, hue="Survived")

料金

fare_survived = df.query("Survived == 1")
fare_not_survived = df.query("Survived == 0")
sns.histplot(x="Fare", data=age_survived, bins=30, kde=True, color="green", alpha=0.7)
sns.histplot(x="Fare", data=age_not_survived, bins=30, kde=True, color="red", alpha=0.5)

出港地

sns.set_style("whitegrid")
sns.countplot(x="Embarked", data=df, hue="Survived")

タイタニックに同乗している兄弟・配偶者の数、親・子供の数が0人以外だと、生存率が高いように見える、など気になるデータもありますが、今回はシンプルに考えたいので扱いません。

今回はデータの中身を調査して、下記のことがわかりました。

  • 上層クラスの生存率が高い
  • 女性の方が生存率が高い
  • 10代の生存率が高い

次回はこれらの項目を元に学習していきたいと思います。

Go の Gin と GORM で API を作る(2)

前回は repository を修正したので、今回はそれを利用して handler を修正していきたいと思います。handler/api/article.go というファイルを新規に作成し、下記のように記述しました。

package handler_api

import (
    "bbs/repository"
    "errors"
    "github.com/gin-gonic/gin"
    "net/http"
    "strconv"
)

// 1ページあたりいくつの記事を表示するか
const perPage = 20

// ArticleRequest 記事投稿リクエストを受ける struct
// バリデーションルールを binding に記述する
// 最大文字数のバリデーション max を追加
type ArticleRequest struct {
    Name string `json:"name" binding:"max=255"`
    Body string `json:"body" binding:"required,max=10000"`
}

// GetArticles 掲示板の記事を取得する
func GetArticles(c *gin.Context) {
    // クエリパラメータ取得
    paramLimit := c.Query("limit")
    paramOffset := c.Query("offset")
    limit, _ := strconv.Atoi(paramLimit)
    offset, _ := strconv.Atoi(paramOffset)
    if limit == 0 {
        limit = perPage
    }

    articles, _, err := repository.GetArticles(limit, offset)
    if err != nil {
        // このエラーハンドリングは次回以降に説明
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // レスポンスは json で返したいので下記のメソッドを利用する
    // レスポンスは repository.Article struct のスライス
    c.JSON(http.StatusOK, articles)
}

// CreateArticle 掲示板の記事を作成する
func CreateArticle(c *gin.Context) {
    // バリデーション&バインド
    var req ArticleRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // DBに保存
    article, _, err := repository.CreateArticle(req.Name, req.Body)
    if err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // レスポンスは repository.Article struct
    c.JSON(http.StatusCreated, article)
}

// UpdateArticle 掲示板の記事を更新する
func UpdateArticle(c *gin.Context) {
    // PathInfo のパラメータを取得
    // ルーティングは下記のように設定する想定 
    // r.PUT("/article/:articleId", handler_api.UpdateArticle)
    paramArticleId := c.Param("articleId")
    articleId, _ := strconv.Atoi(paramArticleId)
    if articleId == 0 {
        err := errors.New("invalid ArticleId")
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // バリデーション&バインド
    var req ArticleRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // DBに保存
    article, _, err := repository.UpdateArticle(articleId, req.Name, req.Body)
    if err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    c.JSON(http.StatusOK, article)
}

// DeleteArticle 掲示板の記事を更新する
func DeleteArticle(c *gin.Context) {
    // PathInfo のパラメータを取得
    paramArticleId := c.Param("articleId")
    articleId, _ := strconv.Atoi(paramArticleId)
    if articleId == 0 {
        err := errors.New("invalid ArticleId")
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    // DBから削除
    _, err := repository.DeleteArticle(articleId)
    if err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    c.JSON(http.StatusOK, nil)
}

説明が必要そうな箇所には、ソース内にコメントを付けました。これで、正常系は問題無く動作することが確認できています。が、異常系で動かしてみると、いろいろと問題がありそうです。具体的には下記のような問題があります。

  • クエリパラメータやPathInfoのパラメータが不正なパラメータのとき、期待通りの挙動にならない
  • 更新・削除をするとき、対象となるレコードが存在しなくても、エラーにならない
  • 更新するとき、キーの無い項目については更新しない、という仕様の方が良い?

次回は上記の問題点を解消していきます。

参考リンク

Go の Gin と GORM で API を作る(1)

Qiita に書いた記事「Go の Gin と GORM で最低限の掲示板を作るチュートリアル」で作った掲示板を改修していきたいと思います。今回は articles テーブルにCRUDするAPIを作っていきたいと思います。この記事では、DB articles テーブルに対するCRUD処理を書く repository/article.go 修正をします。

package repository

import (
    "gorm.io/gorm"
    "time"
)

// Article 記事用の struct
// gorm.Model 構造体を使えば ID, CreatedAt, UpdatedAt, DeletedAt の記述は省略できる
// nullable なカラムについてはポインタ型を指定
type Article struct {
    ID        int
    Name      *string
    Body      string
    CreatedAt *time.Time
    UpdatedAt *time.Time
    DeletedAt gorm.DeletedAt // 論理削除できるよう型を gorm.DeletedAt に変更
    //gorm.Model
}

// GetArticles DBから記事を取得する
func GetArticles(limit int, offset int) ([]Article, *gorm.DB, error) {
    // gorm.DB を取得
    db, err := CreateDB()
    if err != nil {
        return nil, nil, err
    }

    var articles []Article
    // 実行
    // 最新の投稿から表示したいので id の降順で並べ替えておく
    // limit, offset を追加してページネーションに対応
    result := db.Limit(limit).Offset(offset).Order("id desc").Find(&articles)
    if result.Error != nil {
        return nil, nil, result.Error
    }

    return articles, result, nil
}

// CreateArticle DBに記事を保存する
func CreateArticle(name string, body string) (*Article, *gorm.DB, error) {
    // 将来的に Article の中身(カラム)が増えてくれば、引数は中身をひとまとめにした struct が良さそう

    // gorm.DB を取得
    db, err := CreateDB()
    if err != nil {
        return nil, nil, err
    }

    // DBへ投入するデータを作成 エスケープしたものを入れる
    name = template.HTMLEscapeString(name)
    body = template.HTMLEscapeString(body)
    article := Article{
        Name: &name,
        Body: body,
    }

    // 実行
    result := db.Create(&article)
    if result.Error != nil {
        return nil, nil, result.Error
    }

    return &article, result, nil
}

// UpdateArticle 記事を更新する
func UpdateArticle(id int, name string, body string) (*Article, *gorm.DB, error) {
    // gorm.DB を取得
    db, err := CreateDB()
    if err != nil {
        return nil, nil, err
    }

    // 更新するデータを作成 エスケープしたものを使う
    name = template.HTMLEscapeString(name)
    body = template.HTMLEscapeString(body)
    article := Article{
        ID:   id,
        Name: &name,
        Body: body,
    }

    // 実行
    result := db.Save(&article)
    if result.Error != nil {
        return nil, nil, result.Error
    }

    return &article, result, nil
}

// DeleteArticle 記事を削除する
func DeleteArticle(id int) (*gorm.DB, error) {
    // gorm.DB を取得
    db, err := CreateDB()
    if err != nil {
        return nil, err
    }

    // 実行
    article := Article{ID: id}
    result := db.Delete(&article)
    if result.Error != nil {
        return nil, result.Error
    }

    return result, nil
}

UpdateArticle() DeleteArticle() 関数を追加しました。GOAMの書き方そのままですね。削除は論理削除です。次回は handler 側を編集して repository/article.go に作った関数を使いAPIを作成していきます。

【2023/06/18 追記】
エラーハンドリングを修正、Name と Body の内容はエスケープしてから保存するように修正しました。