メインコンテンツへスキップ
見出し画像

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)のため1255\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,408Bias(NN)=node数=256Wights(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が値下がりしてるというけどそれなりにはするな・・・

     

     
     

    KIYO

     
     
    普段は製造業で企画/開発/設計しております。記事はプログラミング・機械学習、IoT関係の記事をメインで作成し、なるべく1つの記事で知りたいことを網羅していきます。内容は学術的より実装・アウトプット(ほしくなるもの)を重視して作成しています。 面白そうな仕事があればやりたいです!

    あなたへのおすすめ