見出し画像

Kaggleコンペの練習:Digit Recognizer

概要

 CNNモデルの練習をしたいため、Kaggleの勉強と合わせて「Digit Recognizer」コンペに参加してみました。

0.分析フロー

 基本的な分析フローはほぼ同じになるため、一度整理しました。

  1. データの準備/タスクの理解

  2. データの前処理

  3. 特徴量エンジニアリング(データの特徴量作成)

  4. データセット/データローダーの作成

  5. AIモデルの選定・作成

  6. ハイパーパラメータの選定(モデルのハイパラ、損失関数、最適化関数、学習率、乱数シード、データ分割数など)

  7. モデルの学習・保存

  8. モデルのチューニング(ハイパーパラメータ調整)

  9. 推論:学習済みモデルでtestデータを予測

  10. 提出・結果確認

1.データの準備/タスクの理解

 今回はKaggleの下記コンペに参加します。練習用コンペのため期限なしで誰でも参加可能です。

1-1.データの確認

 Data詳細は「Data」タブより確認できます。

  • データはグレー画像(1次元)の手書き文字でありラベルは0~9

  • 各画像はwidth=28pixel, height=28pixel, 値は0~255

  • 各データは785columnsあり最初の列がラベル(正解値)となる

  • "The evaluation metric(評価指標)"は精度(正解数)

1-2.データのダウンロード

 Filesは「sample_submission.csv, test.csv, train.csv」の3つです。下記からDLできます。解凍してcsvファイルをモデルを同じディレクトリに保存します。

【参考:Kaggle API】
 API設定済みの方は下記コマンドでもzipファイルをDL可能です

[IN]
!kaggle competitions download -c digit-recognizer

2.データの前処理

 本章の全コードは下記の通りです。

[IN]
import os
import glob
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchinfo import summary
import torchvision

#ファイルのDL
files = glob.glob('*.csv') 
def getfile(datas, name): return [data for data in datas if name in data][0] #指定文字からファイルを取得
path_digits_train, path_digits_test, path_digits_sample = getfile(files, 'train'), getfile(files, 'test'), getfile(files, 'sample') #train, test, sampleのパスを取得
df_digits_train, df_digits_test, df_digits_sample = pd.read_csv(path_digits_train), pd.read_csv(path_digits_test), pd.read_csv(path_digits_sample) #データフレーム化

#前処理-欠損値確認
missvalues_train = df_digits_train.isnull().sum() #欠損値の合計数
missvalues_test = df_digits_test.isnull().sum() #欠損値の合計数
missvalues_sample = df_digits_sample.isnull().sum() #欠損値の合計数
# print('train:', missvalues_train.value_counts())
# print('test:', missvalues_test.value_counts())
# print('sample:', missvalues_sample.value_counts())  #欠損値の合計数の値の種類(0のみなら欠損値なし)

#データとラベルの分割
df_x_train, df_y_train = df_digits_train.drop(columns=['label']), df_digits_train['label'] #dataとlabelに分ける
x_train_linear, y_train_linear = df_x_train.to_numpy(), df_y_train.to_numpy() #Numpy型データ取得
x_train_linear = x_train_linear/255 #正規化(min=0, max=1)

#前処理2:画像の形状操作
x_train_np, y_train_np = x_train_linear.reshape(-1, 28, 28), y_train_linear #y_train_linearは1次元のためそのまま
print(x_train_np.shape, y_train_np.shape, 'MinMax:', x_train_np.min(), x_train_np.max()) #形状確認

[OUT]
(42000, 28, 28) (42000,) MinMax: 0.0 1.0

 2-1ー1.データの内容確認

 まずはデータをPandasで読み込んで中身を確認します。

  • "getfile"関数はリストから指定文字を含む文字列(path)を取得するだけ

  • HorizontalDisplayはpandasのdfを並列表示するだけ

  • trainは1列目がラベル, 2列目以降が各pixel。labelはlabelなし

[IN]
class HorizontalDisplay:
    def __init__(self, *args):
        self.args = args

    def _repr_html_(self):
        template = '<div style="float: left; padding: 10px;">{0}</div>'
        return "\n".join(template.format(arg._repr_html_())
                         for arg in self.args)

files = glob.glob('*.csv') 
def getfile(datas, name): return [data for data in datas if name in data][0] #指定文字からファイルを取得
path_digits_train, path_digits_test, path_digits_sample = getfile(files, 'train'), getfile(files, 'test'), getfile(files, 'sample') #train, test, sampleのパスを取得
df_digits_train, df_digits_test, df_digits_sample = pd.read_csv(path_digits_train), pd.read_csv(path_digits_test), pd.read_csv(path_digits_sample) #データフレーム化

display(HorizontalDisplay(df_digits_train.head(5), df_digits_test.head(5), df_digits_sample.head(5)))

[OUT]

 2-1ー2.欠損値の確認・処理

 決定木モデルなら問題ないですが、全結合モデルは欠損値を処理できないため欠損値を確認して存在するなら処理(埋める、削除など)します。

  • "df_digits_train.isnull().sum()"で各カラム(列)の欠損値の数を計算

  • "value_counts()"メソッドでデータの種類をカウント->欠損値0の数=カラム数であることを確認

[IN]
missvalues_train = df_digits_train.isnull().sum() #欠損値の合計数
missvalues_test = df_digits_test.isnull().sum() #欠損値の合計数
missvalues_sample = df_digits_sample.isnull().sum() #欠損値の合計数
print('train:', missvalues_train.value_counts())
print('test:', missvalues_test.value_counts())
print('sample:', missvalues_sample.value_counts())  #欠損値の合計数の値の種類(0のみなら欠損値なし)

[OUT]
train: 0    785 dtype: int64
test: 0    784 dtype: int64
sample: 0    2 dtype: int64

 上記より欠損値はないためそのまま使用します。

 2-1ー3.入力データ・ラベルへ分割

 入力値とラベルを分けます。

[IN]
df_x_train, df_y_train = df_digits_train.drop(columns=['label']), df_digits_train['label'] #dataとlabelに分ける

[OUT]
-

 2-1ー4.データの正規化・形状変換

 深層学習モデルは入力値は0~1(または-1~1)で最適な学習が可能です。画像データは0~255(1byte)のため$${\frac{1}{255}}$$をかけて正規化します。

[IN]
df_x_train, df_y_train = df_digits_train.drop(columns=['label']), df_digits_train['label'] #dataとlabelに分ける

x_train_linear, y_train_linear = df_x_train.to_numpy(), df_y_train.to_numpy() #Numpy型データ取得
x_train_linear = x_train_linear/255 #正規化(min=0, max=1)

[OUT]

 全結合モデルであれば今のままで問題ないですが、今回はCNN(畳み込み演算)モデルを作成するため入力値を2次元データに変換します。
 この状態で形状(28,28)でmin, max=(0, 1)の入力データが得られました。

[IN]
#前処理2:画像の形状操作
x_train_np, y_train_np = x_train_linear.reshape(-1, 28, 28), y_train_linear #y_train_linearは1次元のためそのまま
print(x_train_np.shape, y_train_np.shape, 'MinMax:', x_train_np.min(), x_train_np.max()) #形状確認

[OUT]
(42000, 28, 28) (42000,) MinMax: 0.0 1.0

【参考:np.array.reshape()の動作確認】

[IN]
a = np.arange(9)
b = a.reshape(3,3) #Numpyのreshapeメソッドの動作確認
print(a)
print(b)

[OUT]
[0 1 2 3 4 5 6 7 8]
[[0 1 2]
 [3 4 5]
 [6 7 8]]

3.特徴量エンジニアリング

 本データでは特徴エンジニアリングは不要(ある種、2次元データ化が特徴エンジに当たると思う)のため省きますが、特にテーブルデータでは必須作業のため、参考記事のせておきます。

4.データセット/データローダーの作成

4-1.データの分割

 通常は過学習を防止するために「train(学習), valid(検証), test(テスト)」用に分割します。今回はデータ数が6万枚ていどのためtrainとvalidのみに分けました。

[IN]
from sklearn.model_selection import train_test_split

#Pytorchを使用するための前処理
x_train_tensor = torch.tensor(x_train_np, dtype=torch.float32) #Tensor型に変換
x_train_tensor = x_train_tensor.unsqueeze(1) #チャネル数を1にする
y_train_tensor = torch.tensor(y_train_np, dtype=torch.int64) #Tensor型に変換
print(x_train_tensor.shape, y_train_tensor.shape)

#データ分割
x_train, x_val, y_train, y_val = train_test_split(x_train_tensor, y_train_tensor, test_size=0.2, random_state=0) #データ分割
print(f'x_train:{x_train.shape}, y_train:{y_train.shape}, x_val:{x_val.shape}, y_val:{y_val.shape}')

[OUT]
torch.Size([42000, 1, 28, 28]) torch.Size([42000])
x_train:torch.Size([33600, 1, 28, 28]), y_train:torch.Size([33600]), x_val:torch.Size([8400, 1, 28, 28]), y_val:torch.Size([8400])

4-2.ミニバッチの作成

 前節は「バッチ学習」のデータでしたが、そのデータを使用してミニバッチを作成します。本当は自作DataLoaderを作成するのがベストですが、今は十分に理解できないため各データ・ラベルでミニバッチを作成しました。
(Shuffleをするとデータとラベルの順番があわないためFalse

【追記】
 検証用データ(x_val, y_val)のミニバッチを追加->CUDAを使うときは学習用だけでなく検証用もミニバッチにしないとCUDAにのらないため必須

[IN]
from torch.utils.data import DataLoader

#GPUの設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 

#GPUへ転送
x_train, x_val, y_train, y_val = x_train.to(device), x_val.to(device), y_train.to(device), y_val.to(device) # GPUへ転送
net= net.to(device) # モデルをGPUへ転送

x_train_minibatch = DataLoader(x_train, batch_size=60, shuffle=False)
y_train_minibatch = DataLoader(y_train, batch_size=60, shuffle=False)
x_val_minibatch = DataLoader(x_val, batch_size=60, shuffle=False)
y_val_minibatch = DataLoader(y_val, batch_size=60, shuffle=False)

[OUT]

5.AIモデルの選定・作成

5-1.モデル設計

 今回は画像データ(+勉強用)のためCNN(畳み込み)モデルを使用します。モデル構造(Architecture)はシンプルなVGGを参考にしました。

 本当は全く同じ構造にしたいのですがMy PC/Colabだと大量のパラメータを学習できない(Errot:cuda out of memory)ためColabでギリギリ学習できるくらいの深さにしました。

【今回のモデルのイメージ図】

5-2.モデルの作成:コード

 今回作成したモデルは下記の通りです。

[IN]
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1) #(in_C, out_C, kernel_size, stride)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=1, padding=1)
        self.relu2 = nn.ReLU(inplace=True)
        self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (14, 14, 16)
        
        self.conv3 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        self.relu3 = nn.ReLU(inplace=True)
        self.conv4 = nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1)
        self.relu4 = nn.ReLU(inplace=True)
        self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (7, 7, 32)
        
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(32*7*7, 256)
        self.relu5 = nn.ReLU(inplace=True)
        self.dropout1 = nn.Dropout(p=0.3)
        self.linear2 = nn.Linear(256, 10)
        
    def forward(self, x):
        out = self.relu1(self.conv1(x))
        out = self.relu2(self.conv2(out))
        out = self.maxpool1(out)
        
        out = self.relu3(self.conv3(out))
        out = self.relu4(self.conv4(out))
        out = self.maxpool2(out)
        
        out = self.avgpool(out)
        out = self.flatten(out)
        out = self.relu5(self.linear1(out))
        out = self.dropout1(out)
        out = self.linear2(out)
        return out 


[OUT]
-

 "torchinfo"でモデル構造を確認します。下記の通り最後の全結合層のところで95.5%のパラメータ数を占めており、いかにCNNのパラメータ効率が良いかが理解できます。

[IN]
summary(Net())

[OUT]
=================================================================
Layer (type:depth-idx)                   Param #
=================================================================
Net                                      --
├─Conv2d: 1-1                            160
├─ReLU: 1-2                              --
├─Conv2d: 1-3                            2,320
├─ReLU: 1-4                              --
├─MaxPool2d: 1-5                         --
├─Conv2d: 1-6                            4,640
├─ReLU: 1-7                              --
├─Conv2d: 1-8                            9,248
├─ReLU: 1-9                              --
├─MaxPool2d: 1-10                        --
├─AdaptiveAvgPool2d: 1-11                --
├─Flatten: 1-12                          --
├─Linear: 1-13                           401,664
├─ReLU: 1-14                             --
├─Dropout: 1-15                          --
├─Linear: 1-16                           2,570
=================================================================
Total params: 420,602
Trainable params: 420,602
Non-trainable params: 0
=================================================================

$$
Wights(NN)=C×H×W×node数=32×7×7×256=401,408\\
Bias(NN)=node数=256
$$

6.ハイパーパラメータの選定

 通常のハイパーパラメータはモデル内のパラメータですが、ここでは精度を良くするためのパラメータとして下記を設定します。

  • 最適化関数:Adam

  • 損失関数(目的関数):交差エントロピー

  • 学習率η

  • 学習回数(Epoch数)

[IN]
#学習用パラメータの設定
epochs = 1000
lr = 0.01

def logging_epoch(logs, epoch, loss, accuracy): #学習結果を辞書に格納する関数 
    logs['epoch'].append(epoch) #学習回数を格納 
    logs['loss'].append(loss) #損失関数を格納 
    logs['accuracy'].append(accuracy) #正解率を格納

logs = {'train':{'epoch':[], 'loss':[], 'accuracy':[]},
        'val':{'epoch':[], 'loss':[], 'accuracy':[]}} #学習結果を格納する辞書


net = Net()
optimizer = optim.Adam(net.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss() #損失関数の設定:クロスエントロピー誤差

[OUT]
-

7.モデルの学習・保存

7-1.GPUへの転送

 本モデルはGPUがないとまともに学習できないため変数をGPUに乗せます(GPUありで30~60min程度)。

[IN]
#GPUの設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 
# device = 'cpu' #RuntimeError: CUDA out of memory.が取れない

#GPUへ転送
x_train, x_val, y_train, y_val = x_train.to(device), x_val.to(device), y_train.to(device), y_val.to(device) # GPUへ転送
net= net.to(device) # モデルをGPUへ転送

[OUT]

7-2.学習・検証フェーズ

 それでは学習に入ります。学習では下記がありますが今回はバッチ学習とミニバッチ学習を実施しました。
 また学習時にはグリッドサーチなどの手法が有効ですが今回は飛ばしています。

  • オンライン学習:1行ずつのデータで学習

  • バッチ学習:全データ(バッチ)を使用して学習

  • ミニバッチ学習:データを分割(ミニバッチ)

【バッチ学習】

[IN]
for epoch in tqdm(range(epochs)):
    for phase in ['train', 'val']:
        if phase == 'train':
            net.train() # モデルを訓練モードに
        else:
            net.eval() # モデルを評価モードに

        optimizer.zero_grad() # 勾配の初期化
  
        if phase == 'train':
            output = net(x_train) # モデルの出力
            loss = criterion(output, y_train) # 損失の計算
            loss.backward() # 勾配の計算
            optimizer.step() # パラメータの更新
    
        if epoch % 10 ==0:
            predicted = torch.max(output, 1)[1] # 予測ラベル
            
            if phase == 'train':
                train_acc = (predicted == y_train).sum()  / len(y_train)
                logging_epoch(logs=logs['train'], epoch=epoch, loss=loss.item(), accuracy=train_acc.item()) # 学習データの結果を格納
                
            elif phase == 'val':    
                y_pred_val = net(x_val) # テストデータの予測
                predicted = torch.max(y_pred_val, 1)[1] # 予測ラベル
                val_acc =  (predicted == y_val).sum() / len(y_val) # 正解率の計算
                
                loss_val = criterion(y_pred_val, y_val) # テストデータの損失
                logging_epoch(logs=logs['test'], epoch=epoch, loss=loss_val.item(), accuracy=val_acc.item()) # テストデータの結果を格納
        
[OUT]
-

【ミニバッチ学習】

  • "CUDA out of memory"エラーの時はミニバッチのサイズを小さくする

  • "CUDA out of memory"字はtrainだけでなくvalのバッチサイズも要調整

[IN]
for epoch in tqdm(range(epochs)):
    train_loss, train_acc  = 0, 0
    val_loss, val_acc  = 0, 0
    
    #訓練フェーズ
    net.train()
    count = 0
    
    for imgs, labels in zip(x_train_minibatch, y_train_minibatch):
        count += len(labels) #データ数をカウント
        print(f'epoch: {epoch}, count: {count}, len(labels): {len(labels)}')
        
        #学習フェーズ
        optimizer.zero_grad() #勾配の初期化
        outputs = net(imgs) #順伝播(出力の計算)
        loss = criterion(outputs, labels) #損失関数の計算
        loss.backward()
        optimizer.step() #パラメータ更新
        
        #ロギングデータの更新
        train_loss += loss.item() #損失関数の合計を計算
        y_pred = torch.max(outputs, 1)[1]
        train_acc += (y_pred == labels).sum().item() #正解数を計算
        
        loss_train_avg = train_loss / count #損失関数の平均を計算
        loss_acc_avg = train_acc / count #正解率の平均を計算
    
    #検証フェーズ
    net.eval()
    count = 0
    
    for imgs, labels in zip(x_val_minibatch, y_val_minibatch):
        count += len(labels) #データ数をカウント

        #推論フェーズ
        outputs = net(imgs) #順伝播(出力の計算)
        loss = criterion(outputs, labels) #損失関数の計算

        #ロギングデータの更新
        val_loss += loss.item() #損失関数の合計を計算
        y_pred = torch.max(outputs, 1)[1]
        val_acc += (y_pred == labels).sum().item() #正解数を計算

        loss_val_avg = val_loss / count #損失関数の平均を計算
        loss_acc_avg = val_acc / count #正解率の平均を計算
    
    #学習結果の表示/ロギング
    if epoch % 10 == 0:
        print(f'epoch: {epoch}, loss_train: {loss_train_avg:.4f}, loss_val: {loss_val_avg:.4f}, acc_train: {loss_acc_avg:.4f}, acc_val: {loss_acc_avg:.4f}')
        logging_epoch(logs['train'], epoch=epoch, loss=loss_train_avg, accuracy=loss_acc_avg)
        logging_epoch(logs['val'], epoch=epoch, loss=loss_val_avg, accuracy=loss_acc_avg)
        
[OUT]

7-3.ログの可視化

 取得したログを可視化します(※epoch0のvalは1回目学習後の結果のためtrainよりよくなる)。

[IN]
def plot_logs(logs): #学習結果のグラフ化
    fig, ax = plt.subplots(1, 2, figsize=(15, 5))
    #損失関数のグラフ化
    ax[0].plot(logs['train']['epoch'], logs['train']['loss'], label='train', ls='--') #train
    ax[0].plot(logs['val']['epoch'], logs['val']['loss'], label='val') #val
    ax[0].set_xlabel('epoch')
    ax[0].set_ylabel('loss')
    ax[0].set_title('Time series of Loss')
    ax[0].legend(), ax[0].grid()
    #正解率のグラフ化
    ax[1].plot(logs['train']['epoch'], logs['train']['accuracy'], label='train', ls='--') #train
    ax[1].plot(logs['val']['epoch'], logs['val']['accuracy'], label='val') #val
    ax[1].set_xlabel('epoch')
    ax[1].set_ylabel('accuracy')
    ax[1].set_title('Time series of Accuracy')
    ax[1].legend(), ax[1].grid()
    plt.show()
    
plot_logs(logs)

[OUT]

7-4.モデルの保存

 作成したモデル(+学習履歴logs)を保存します。モデルの保存はtorch.save()を使用しますが、modelそのものをsave()しても後で読み込めなかったためパラメータの保存しました。
 Google Colabではセッション完了後にファイルが消えるためローカルPCにダウンロードします

[IN]
torch.save(net.state_dict(), 'model_CNNdigits_lr0.01_batch.pth')
joblib.dump(logs, 'logs_lr0.01_batch.pkl')

[OUT]

8.モデルのチューニング

 XGBoostやSVMなどではハイパーパラメータのチューニングは必要ですが、今回のCNNでは比較的良い精度が出たため実施しておりません。
 参考までに通常実施するならOptunaが良く使われます。

9.推論

9-1.学習済みモデルの呼び出し

 学習直後にそのモデルを使用するなら不要な項目となります。
 Google Colabで学習したモデルをローカルで実施したい場合は①torch.load()でモデルパラメータを呼び出し、②作成したモデルのパラメータを上書きします。

[IN]
net = Net()

path_model ='models/model_CNNdigits.pth' #保存したモデルのパス
model_params = torch.load(path_model)
net.load_state_dict(model_params)

[OUT]
<All keys matched successfully>

9-2.テスト用データへの前処理

 当たり前ではありますがよく忘れるため一つの節として作成しました。
 学習済みモデルで推論する前に学習済みモデルで実施した前処理をテストデータにも実施します。

[IN]
columns = df_digits_sample.columns

#前処理
x_test_linear = df_digits_test.to_numpy() #Numpy型データ取得
x_test_linear = x_test_linear/255 #正規化(min=0, max=1)
x_test_np = x_test_linear.reshape(-1, 28, 28) #y_train_linearは1次元のためそのまま
x_test_tensor = torch.tensor(x_test_np, dtype=torch.float32) #テンソル化
x_test = x_test_tensor.unsqueeze(1) #チャネル数を1にする
print(type(x_test), x_test.shape)

[OUT]
<class 'torch.Tensor'> torch.Size([28000, 1, 28, 28])

9-3.推論+提出用データの作成

 最後に①テストデータで推論、②指定の提出様式(csv)に変換します。

[IN]
#学習済みモデルで予測
outputs = net(x_test) #予測
predicted_test = torch.max(outputs, 1)[1] #予測結果

pred = [i.item() for i in predicted_test]
df = pd.DataFrame([np.arange(1, len(pred)+1), pred]).T
df.columns = columns
display(df.head(3))
df.to_csv('digitsrecog_1stcommit_lr0.01batch.csv', index=False)

[OUT]

10.提出・結果確認

 データ提出は「Submmit Predictions」からStep1,2を実施して「Make Submisson」を実行することで提出可能です。結果はすぐに確認できます。

 結果は下記の通りです。

  • バッチ学習でもそこそこの精度は出た

  • ミニバッチ学習(batchsize=50)の方がより高い精度がでた。batchsizeはハイパラ調整の一つと考えてよい。


11.全コード

 モデルの保存は記載したが読み込みは除外

11-1.バッチ学習

!pip install torchinfo

from google.colab import drive
drive.mount('/content/drive')
import os
import glob
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import joblib

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchinfo import summary
import torchvision


#ファイルのDL
files = glob.glob('/content/drive/MyDrive/11. コンペ/digits_recog_Kaggle/*.csv') 
def getfile(datas, name): return [data for data in datas if name in data][0] #指定文字からファイルを取得
path_digits_train, path_digits_test, path_digits_sample = getfile(files, 'train'), getfile(files, 'test'), getfile(files, 'sample') #train, test, sampleのパスを取得
df_digits_train, df_digits_test, df_digits_sample = pd.read_csv(path_digits_train), pd.read_csv(path_digits_test), pd.read_csv(path_digits_sample) #データフレーム化

#前処理-欠損値確認
missvalues_train = df_digits_train.isnull().sum() #欠損値の合計数
missvalues_test = df_digits_test.isnull().sum() #欠損値の合計数
missvalues_sample = df_digits_sample.isnull().sum() #欠損値の合計数
# print('train:', missvalues_train.value_counts())
# print('test:', missvalues_test.value_counts())
# print('sample:', missvalues_sample.value_counts())  #欠損値の合計数の値の種類(0のみなら欠損値なし)

#データとラベルの分割
df_x_train, df_y_train = df_digits_train.drop(columns=['label']), df_digits_train['label'] #dataとlabelに分ける
x_train_linear, y_train_linear = df_x_train.to_numpy(), df_y_train.to_numpy() #Numpy型データ取得
x_train_linear = x_train_linear/255 #正規化(min=0, max=1)

#前処理2:画像の形状操作
# np.arange(28*28).reshape(28,28) #Numpyのreshapeメソッドの動作確認
x_train_np, y_train_np = x_train_linear.reshape(-1, 28, 28), y_train_linear #y_train_linearは1次元のためそのまま
print(x_train_np.shape, y_train_np.shape, 'MinMax:', x_train_np.min(), x_train_np.max()) #形状確認

from sklearn.model_selection import train_test_split

#Pytorchを使用するための前処理
x_train_tensor = torch.tensor(x_train_np, dtype=torch.float32) #Tensor型に変換
x_train_tensor = x_train_tensor.unsqueeze(1) #チャネル数を1にする
y_train_tensor = torch.tensor(y_train_np, dtype=torch.int64) #Tensor型に変換
print(x_train_tensor.shape, y_train_tensor.shape)

#データ分割
x_train, x_val, y_train, y_val = train_test_split(x_train_tensor, y_train_tensor, test_size=0.2, random_state=0) #データ分割
print(f'x_train:{x_train.shape}, y_train:{y_train.shape}, x_val:{x_val.shape}, y_val:{y_val.shape}')
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1) #(in_C, out_C, kernel_size, stride)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=1, padding=1)
        self.relu2 = nn.ReLU(inplace=True)
        self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (14, 14, 16)
        
        self.conv3 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        self.relu3 = nn.ReLU(inplace=True)
        self.conv4 = nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1)
        self.relu4 = nn.ReLU(inplace=True)
        self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (7, 7, 32)
        
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(32*7*7, 256)
        self.relu5 = nn.ReLU(inplace=True)
        self.dropout1 = nn.Dropout(p=0.3)
        self.linear2 = nn.Linear(256, 10)
        
    def forward(self, x):
        out = self.relu1(self.conv1(x))
        out = self.relu2(self.conv2(out))
        out = self.maxpool1(out)
        
        out = self.relu3(self.conv3(out))
        out = self.relu4(self.conv4(out))
        out = self.maxpool2(out)
        
        out = self.avgpool(out)
        out = self.flatten(out)
        out = self.relu5(self.linear1(out))
        out = self.dropout1(out)
        out = self.linear2(out)
        return out 

#学習用パラメータの設定
epochs = 1000
lr = 0.001

def logging_epoch(logs, epoch, loss, accuracy): #学習結果を辞書に格納する関数 
    logs['epoch'].append(epoch) #学習回数を格納 
    logs['loss'].append(loss) #損失関数を格納 
    logs['accuracy'].append(accuracy) #正解率を格納

logs = {'train':{'epoch':[], 'loss':[], 'accuracy':[]},
        'test':{'epoch':[], 'loss':[], 'accuracy':[]}} #学習結果を格納する辞書

net = Net()
optimizer = optim.Adam(net.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss() #損失関数の設定:クロスエントロピー誤差

#GPUの設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 
# device = 'cpu' #RuntimeError: CUDA out of memory.が取れない

#GPUへ転送
x_train, x_val, y_train, y_val = x_train.to(device), x_val.to(device), y_train.to(device), y_val.to(device) # GPUへ転送
net= net.to(device) # モデルをGPUへ転送
for epoch in tqdm(range(epochs)):
    for phase in ['train', 'val']:
        if phase == 'train':
            net.train() # モデルを訓練モードに
        else:
            net.eval() # モデルを評価モードに

        optimizer.zero_grad() # 勾配の初期化
  
        if phase == 'train':
            output = net(x_train) # モデルの出力
            loss = criterion(output, y_train) # 損失の計算
            loss.backward() # 勾配の計算
            optimizer.step() # パラメータの更新
    
        if epoch % 10 ==0:
            predicted = torch.max(output, 1)[1] # 予測ラベル
            
            if phase == 'train':
                train_acc = (predicted == y_train).sum()  / len(y_train)
                logging_epoch(logs=logs['train'], epoch=epoch, loss=loss.item(), accuracy=train_acc.item()) # 学習データの結果を格納
                
            elif phase == 'val':    
                y_pred_val = net(x_val) # テストデータの予測
                predicted = torch.max(y_pred_val, 1)[1] # 予測ラベル
                val_acc =  (predicted == y_val).sum() / len(y_val) # 正解率の計算
                
                loss_val = criterion(y_pred_val, y_val) # テストデータの損失
                logging_epoch(logs=logs['test'], epoch=epoch, loss=loss_val.item(), accuracy=val_acc.item()) # テストデータの結果を格納
        
torch.save(net.state_dict(), 'model_CNNdigits_lr0.01_batch.pth')
joblib.dump(logs, 'logs_digits_lr0.01_batch.pkl')
columns = df_digits_sample.columns

#前処理
x_test_linear = df_digits_test.to_numpy() #Numpy型データ取得
x_test_linear = x_test_linear/255 #正規化(min=0, max=1)
x_test_np = x_test_linear.reshape(-1, 28, 28) #y_train_linearは1次元のためそのまま
x_test_tensor = torch.tensor(x_test_np, dtype=torch.float32) #テンソル化
x_test = x_test_tensor.unsqueeze(1) #チャネル数を1にする
print(type(x_test), x_test.shape)

#学習済みモデルで予測
outputs = net(x_test) #予測
predicted_test = torch.max(outputs, 1)[1] #予測結果

pred = [i.item() for i in predicted_test]
df = pd.DataFrame([np.arange(1, len(pred)+1), pred]).T
df.columns = columns
display(df.head(3))
df.to_csv('digitsrecog_1stcommit_lr0.01batch.csv', index=False)

11-2.ミニバッチ学習

 初回のミニバッチ学習のやり方を間違えていた(検証データのミニバッチをしてなかった)ため、モデル作り直しました。下記コードで99%まで行きましたのでこちらを紹介します。

!pip install torchinfo

from google.colab import drive
drive.mount('/content/drive')
import os
import glob
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchinfo import summary
import torchvision
from torch.utils.data import DataLoader
import torch.optim as optim
from tqdm import tqdm

#ファイルのDL
files = glob.glob('*.csv') 
def getfile(datas, name): return [data for data in datas if name in data][0] #指定文字からファイルを取得
path_digits_train, path_digits_test, path_digits_sample = getfile(files, 'train'), getfile(files, 'test'), getfile(files, 'sample') #train, test, sampleのパスを取得
df_digits_train, df_digits_test, df_digits_sample = pd.read_csv(path_digits_train), pd.read_csv(path_digits_test), pd.read_csv(path_digits_sample) #データフレーム化

#前処理-欠損値確認
missvalues_train = df_digits_train.isnull().sum() #欠損値の合計数
missvalues_test = df_digits_test.isnull().sum() #欠損値の合計数
missvalues_sample = df_digits_sample.isnull().sum() #欠損値の合計数
# print('train:', missvalues_train.value_counts())
# print('test:', missvalues_test.value_counts())
# print('sample:', missvalues_sample.value_counts())  #欠損値の合計数の値の種類(0のみなら欠損値なし)

#データとラベルの分割
df_x_train, df_y_train = df_digits_train.drop(columns=['label']), df_digits_train['label'] #dataとlabelに分ける
x_train_linear, y_train_linear = df_x_train.to_numpy(), df_y_train.to_numpy() #Numpy型データ取得
x_train_linear = x_train_linear/255 #正規化(min=0, max=1)

#前処理2:画像の形状操作
# np.arange(28*28).reshape(28,28) #Numpyのreshapeメソッドの動作確認
x_train_np, y_train_np = x_train_linear.reshape(-1, 28, 28), y_train_linear #y_train_linearは1次元のためそのまま
print(x_train_np.shape, y_train_np.shape, 'MinMax:', x_train_np.min(), x_train_np.max()) #形状確認

#Pytorchを使用するための前処理
x_train_tensor = torch.tensor(x_train_np, dtype=torch.float32) #Tensor型に変換
x_train_tensor = x_train_tensor.unsqueeze(1) #チャネル数を1にする
y_train_tensor = torch.tensor(y_train_np, dtype=torch.int64) #Tensor型に変換
print(x_train_tensor.shape, y_train_tensor.shape)


#データ分割
x_train, x_val, y_train, y_val = train_test_split(x_train_tensor, y_train_tensor, test_size=0.2, random_state=0) #データ分割
print(f'x_train:{x_train.shape}, y_train:{y_train.shape}, x_val:{x_val.shape}, y_val:{y_val.shape}')
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.conv1 = nn.Conv2d(1, 64, kernel_size=3, stride=1, padding=1) #(in_C, out_C, kernel_size, stride)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
        self.relu2 = nn.ReLU(inplace=True)
        self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (14, 14, 64)
        
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
        self.relu3 = nn.ReLU(inplace=True)
        self.conv4 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
        self.relu4 = nn.ReLU(inplace=True)
        self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2) #outputsize(h, w, c) = (7, 7, 128)
        
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(128*7*7, 512)
        self.relu5 = nn.ReLU(inplace=True)
        self.dropout1 = nn.Dropout(p=0.3)
        self.linear2 = nn.Linear(512, 10)
        
    def forward(self, x):
        out = self.relu1(self.conv1(x))
        out = self.relu2(self.conv2(out))
        out = self.maxpool1(out)
        
        out = self.relu3(self.conv3(out))
        out = self.relu4(self.conv4(out))
        out = self.maxpool2(out)
        
        out = self.avgpool(out)
        out = self.flatten(out)
        out = self.relu5(self.linear1(out))
        out = self.dropout1(out)
        out = self.linear2(out)
        return out 
def logging_epoch(logs, epoch, loss, accuracy): #学習結果を辞書に格納する関数 
    logs['epoch'].append(epoch) #学習回数を格納 
    logs['loss'].append(loss) #損失関数を格納 
    logs['accuracy'].append(accuracy) #正解率を格納

logs = {'train':{'epoch':[], 'loss':[], 'accuracy':[]},
        'val':{'epoch':[], 'loss':[], 'accuracy':[]}} #学習結果を格納する辞書

def plot_logs(logs): #学習結果のグラフ化
    fig, ax = plt.subplots(1, 2, figsize=(15, 5))
    #損失関数のグラフ化
    ax[0].plot(logs['train']['epoch'], logs['train']['loss'], label='train', ls='--') #train
    ax[0].plot(logs['val']['epoch'], logs['val']['loss'], label='val') #val
    ax[0].set_xlabel('epoch')
    ax[0].set_ylabel('loss')
    ax[0].set_title('Time series of Loss')
    ax[0].legend(), ax[0].grid()
    #正解率のグラフ化
    ax[1].plot(logs['train']['epoch'], logs['train']['accuracy'], label='train', ls='--') #train
    ax[1].plot(logs['val']['epoch'], logs['val']['accuracy'], label='val') #val
    ax[1].set_xlabel('epoch')
    ax[1].set_ylabel('accuracy')
    ax[1].set_title('Time series of Accuracy')
    ax[1].legend(), ax[1].grid()
    plt.show()
#学習用パラメータの設定
epochs = 1000
lr = 0.001

#モデルの定義
net = Net()
optimizer = optim.Adam(net.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss() #損失関数の設定:クロスエントロピー誤差

#GPUの設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

#GPUへ転送
x_train, x_val, y_train, y_val = x_train.to(device), x_val.to(device), y_train.to(device), y_val.to(device) # GPUへ転送

x_train_minibatch = DataLoader(x_train, batch_size=60, shuffle=False)
y_train_minibatch = DataLoader(y_train, batch_size=60, shuffle=False)
x_val_minibatch = DataLoader(x_val, batch_size=60, shuffle=False)
y_val_minibatch = DataLoader(y_val, batch_size=60, shuffle=False)

net= net.to(device) # モデルをGPUへ転送
for epoch in tqdm(range(epochs)):
    train_loss, train_acc  = 0, 0
    val_loss, val_acc  = 0, 0
    
    #訓練フェーズ
    net.train()
    count = 0
    
    for imgs, labels in zip(x_train_minibatch, y_train_minibatch):
        count += len(labels) #データ数をカウント
        print(f'epoch: {epoch}, count: {count}, len(labels): {len(labels)}')
        
        #学習フェーズ
        optimizer.zero_grad() #勾配の初期化
        outputs = net(imgs) #順伝播(出力の計算)
        loss = criterion(outputs, labels) #損失関数の計算
        loss.backward()
        optimizer.step() #パラメータ更新
        
        #ロギングデータの更新
        train_loss += loss.item() #損失関数の合計を計算
        y_pred = torch.max(outputs, 1)[1]
        train_acc += (y_pred == labels).sum().item() #正解数を計算
        
        loss_train_avg = train_loss / count #損失関数の平均を計算
        loss_acc_avg = train_acc / count #正解率の平均を計算
    
    #検証フェーズ
    net.eval()
    count = 0
    
    for imgs, labels in zip(x_val_minibatch, y_val_minibatch):
        count += len(labels) #データ数をカウント

        #推論フェーズ
        outputs = net(imgs) #順伝播(出力の計算)
        loss = criterion(outputs, labels) #損失関数の計算

        #ロギングデータの更新
        val_loss += loss.item() #損失関数の合計を計算
        y_pred = torch.max(outputs, 1)[1]
        val_acc += (y_pred == labels).sum().item() #正解数を計算

        loss_val_avg = val_loss / count #損失関数の平均を計算
        loss_acc_avg = val_acc / count #正解率の平均を計算
    
    #学習結果の表示/ロギング
    if epoch % 10 == 0:
        print(f'epoch: {epoch}, loss_train: {loss_train_avg:.4f}, loss_val: {loss_val_avg:.4f}, acc_train: {loss_acc_avg:.4f}, acc_val: {loss_acc_avg:.4f}')
        logging_epoch(logs['train'], epoch=epoch, loss=loss_train_avg, accuracy=loss_acc_avg)
        logging_epoch(logs['val'], epoch=epoch, loss=loss_val_avg, accuracy=loss_acc_avg)
    
plot_logs(logs)
torch.save(net.state_dict(), 'models/params_CNN.pth') #学習済みモデルの保存

推論以降は同じ


別添

別添1.反省

  • logsはとったけど可視化の癖はつけておいた方がよい

  • できるのであれば再現性を保つためにシードは固定して、シード値も管理する(乱数値の影響も大きな要因のため)

  • 練習用ならKaggleのCodeを最初に勉強しておいた方が良かったかも?

  • CUDAにメモリが全然乗らないけど何か間違っている可能性があるか??ー>検証用データもミニバッチ化してCUDAへのメモリを分散すること

  • チーティングだけどKaggleに提供されていないMNISTのデータを学習して提出するとscore1になっているCODEもあった。

別添2.改善点

 より精度を上げられそうなポイントは下記の通り

  • データ拡張の追加(Data Augument:回転・移動・反転)

  • ハイパーパラメータ調整(Optunaなど)


参考資料

あとがき

 GPUがないと何もできんけど、Colabの無料枠だとすぐ止まるし・・・・
GPUが値下がりしてるというけどそれなりにはするな・・・

 

この記事が気に入ったらサポートをしてみませんか?