子供の書き込み済ドリル自動除去_ver2(失敗)

アプリ開発

はじめに

子供がサピックスに通っており、一番書き込みを消したいサピックスのドリルに挑戦したが敗北した。もうこれ以上、深追いすると時間がもったいないと思い、あきらめる。出来た部分をまとめる。

出来栄え

上手くいったバージョンは前記事にまとめている
一方、サピックスのテキストの場合、
【before】

【after】

となり、消えてほしくない箇所が消えている。
印字がくっきりしているものだと消せているかなという印象。

プログラム

こちらのページを参考に作った。

importのセット。いらないものもあるかな。

from torch import nn
import random

import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

from PIL import Image
from torchvision.io import write_jpeg, write_png

import cv2
import os
import shutil
import glob

import torchvision.transforms.functional as F

学習モデル。そのまま使わせていただいた。

#学習モデル
class DnCNN(nn.Sequential):
    def __init__(self,cn=3):
        super(DnCNN,self).__init__()

        self.add_module('coba1',nn.Sequential(
                nn.Conv2d(cn,64,3,stride=1,padding=1),
                nn.ReLU(inplace=True),
            )
        )

        for i in range(2,17):
            self.add_module('coba%d'%i,
                nn.Sequential(
                    nn.Conv2d(64,64,3,stride=1,padding=1),
                    nn.BatchNorm2d(64),
                    nn.ReLU(inplace=True),
                )
            )

        self.add_module('coba17',nn.Conv2d(64,cn,3,stride=1,padding=1))

        for l in self.modules(): # 重みの初期値
            if(type(l)==nn.Conv2d):
                nn.init.kaiming_normal_(l.weight.data)
                l.bias.data.zero_()
            elif(type(l)==nn.BatchNorm2d):
                l.weight.data.fill_(1)
                l.bias.data.zero_()

データセット。ちょっと苦戦したのは、data_dirの下のさら下にフォルダを作るところ。train_rakuとかでフォルダ名を定義しており、そこの直下にデータをおけばよいというわけではない。

## データの準備
transform_train = transforms.Compose([
  transforms.RandomHorizontalFlip(p=0.35), 
  transforms.RandomVerticalFlip(p=0.35),
  transforms.ToTensor(),
  transforms.Normalize(0.5, 0.5), 
  #transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)
])

transform_test = transforms.Compose([
  transforms.ToTensor(),
  transforms.Normalize(0.5, 0.5), 
])

# 訓練データディレクトリと検証データディレクトリの指定
# ツリーのベースディレクトリ
data_dir = 'issiki'

train_raku_dir = os.path.join(data_dir, "train_raku")
train_OK_dir = os.path.join(data_dir, "train_OK")
test_raku_dir = os.path.join(data_dir, "test_raku")
test_OK_dir = os.path.join(data_dir, "test_OK")

## train_raku
# Imagefolder
train_raku_data = datasets.ImageFolder(train_raku_dir, 
            transform=transform_train)
# loader
train_raku_loader = DataLoader(train_raku_data, 
      batch_size=10, shuffle=False)

## train_OK
# Imagefolder
train_OK_data = datasets.ImageFolder(train_OK_dir, 
            transform=transform_train)
# loader
train_OK_loader = DataLoader(train_OK_data, 
      batch_size=10, shuffle=False)

## test_raku
# Imagefolder
test_raku_data = datasets.ImageFolder(test_raku_dir, 
            transform=transform_test)
# loader
test_raku_loader = DataLoader(test_raku_data, 
      batch_size=10, shuffle=False)

## test_OK
# Imagefolder
test_OK_data = datasets.ImageFolder(test_OK_dir, 
            transform=transform_test)
# loader
test_OK_loader = DataLoader(test_OK_data, 
      batch_size=10, shuffle=False)

学習部分。書き込みありとなしを比べるロジックに苦労した。tqdmでデータセットを読み取らせる一方、正解データも書き込みデータセットと対応するような順番で読み取らせなくてはならない。ファイル名やフォルダ名を合わせて作ったからか、一応できたと思う。あと、データセットに対し、transformで加工を施しているが、加工がランダムで入り、かつ正解と書き込みで同じ処理が入るように乱数の初期化をfix_seedでやっている。確認のためshow_images_labelsで画像を出力。

# ランダムの関数
def fix_seed(seed):
    # random
    random.seed(seed)
    # Numpy
    np.random.seed(seed)
    # Pytorch
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.use_deterministic_algorithm = True

# 可視化用
# イメージとラベル表示
def show_images_labels(loader, classes, net, device, num_con):

    # データローダーから最初の1セットを取得する
    for images, labels in loader:
        break
    # 表示数は50個とバッチサイズのうち小さい方
    n_size = min(len(images), 9)

    if net is not None:
      # デバイスの割り当て
      inputs = images.to(device)

      # 予測計算
      outputs = net(inputs)
      images = outputs.detach().to('cpu')

    # 最初のn_size個の表示
    fig=plt.figure(figsize=(20, 15))
    for i in range(n_size):
        ax = plt.subplot(3, 3, i + 1)
        # TensorをNumPyに変換
        image_np = images[i].numpy().copy()
        # 軸の順番変更 (channel, row, column) -> (row, column, channel)
        img = np.transpose(image_np, (1, 2, 0))
        # 値の範囲を[-1, 1] -> [0, 1]に戻す
        img = (img + 1)/2
        # 結果表示
        plt.imshow(img)
        ax.set_axis_off()
    fig.savefig("tmp/"+str(num_con)+".png")

# 学習用関数
def fit(net, optimizer, criterion, num_epochs,
        train_raku_load, train_OK_load, test_raku_load, test_OK_load, device, history):
    print("start")

    # 訓練されたパラメータと結果を保存するファイル
    netparam_file = os.path.join(os.getcwd(),'netparam.pkl')
    if(os.path.exists(netparam_file)):
        # セーブしておいたデータがすでにある場合は先回から続き
        s = torch.load(netparam_file)
        net.load_state_dict(s['w'])
        optimizer.load_state_dict(s['o'])
        save_count = s['n']
        avg_train_loss = s['l']
    else:
        # 最初から開始
        save_count = 0
        avg_train_loss = 0

    # tqdmライブラリのインポート
    from tqdm.notebook import tqdm

    base_epochs = len(history)
    BatchSize = len(train_raku_load)

    for epoch in range(base_epochs, num_epochs+base_epochs):
        train_loss = 0
        val_loss = 0

        #訓練フェーズ
        net.train()
        count = 0

        # seed固定
        SEED=random.randint(0,1000)
        fix_seed(SEED)

        train_OK=iter(train_OK_load)

        # ミニバッチ数分を一気に扱う。epochは訓練、検証の一連で1回
        for raku, label_raku in tqdm(train_raku_load):
            #print("001")
            #print(torch.cuda.memory_reserved(device=device))

            count += 1
            raku = raku.to(device)

            fix_seed(SEED)
            OK, label_OK=next(train_OK)
            OK= OK.to(device)

            # 勾配の初期化
            optimizer.zero_grad()

            # 予測計算
            outputs_raku = net(raku)

            # 損失計算
            loss = criterion(outputs_raku, OK)
            train_loss += loss.item()

            #print("006")
            # 勾配計算
            loss.backward()

            # パラメータ修正
            optimizer.step()

            # 損失と精度の計算
            avg_train_loss = train_loss / count

            # seed固定
            SEED=random.randint(0,1000)
            fix_seed(SEED)

        # パラメータや状態を保存する
        sd = dict(w=net.state_dict(),o=optimizer.state_dict(),n=save_count+count,l=avg_train_loss)
        torch.save(sd,os.path.join(os.getcwd(),'netparam.pkl'))

        #予測フェーズ
        net.eval()
        count = 0

        for raku_data, OK_data in zip(test_raku_load, test_OK_load):
            count += 1
            raku, raku_label=raku_data
            OK, OK_label=OK_data

            #print("007")
            #print(torch.cuda.memory_reserved(device=device))
            raku = raku.to(device)
            OK = OK.to(device)

            # 予測計算
            outputs_raku = net(raku)

            # 損失計算
            loss = criterion(outputs_raku, OK)
            val_loss += loss.item()

            #print("010")
            # 損失と精度の計算
            avg_val_loss = val_loss / count

        print (f'Epoch [{(epoch+1)}/{num_epochs+base_epochs}], test_loss: {avg_train_loss:.5f}  val_loss: {avg_val_loss:.5f}')
        item = np.array([epoch+1, avg_train_loss, avg_val_loss])
        history = np.vstack((history, item))
        #print("013")

        with open('tmp.csv', 'a') as f_handle:
          np.savetxt(f_handle, item.reshape(-1,3), delimiter=',')

        show_images_labels(test_raku_load, None, net, device, epoch)
        #print("014")

    return history

プログラム実行部分。csvファイルがない場合は自動で作るようにした。

criterion=nn.MSELoss()
lr=0.01
#0.0001 ~10
#0.01
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net=DnCNN().to(device)

print(device)
optimizer=optim.Adam(net.parameters(),lr=lr)
num_epochs=10
#history2=np.zeros((0,3))

# historyファイル
history_file = os.path.join(os.getcwd(),'tmp.csv')
if(os.path.exists(history_file)):
    # セーブしておいたデータがすでにある場合は先回から続き
    history2=np.loadtxt('tmp.csv', delimiter=',')
    history2=history2.reshape(-1,3)
else:
    # 最初から開始
    history2=np.zeros((0,3))
    np.savetxt("tmp.csv",history2, delimiter=",")

history2=fit(net, optimizer, criterion, num_epochs,
        train_raku_loader, train_OK_loader, test_raku_loader, test_OK_loader, device, history2)

おわりに

これでも色々やってみてはいる。

  • U-NETで手書き部分だけ分離できないか?→分離できず、、真っ暗。
  • 学習の材料を増やしてみる→効果出ず

人がどのように認識しているのかに立ち返ると、形状や色だけでなく書いている事柄で判断しているのではと思えてくる。
つまり、テキスト解析も込みじゃないと本当の意味で出来ないのか?

参考資料

pytorchでdeep learningの本を探してこちらを見つけたが、なかなか良かったと思う。今まで見てきた本は、同じことを色々なライブラリを使ってできるという紹介だったが、こちらの本はライブラリが行っている中身も解説してくれてたので、応用が効きやすい。何度も読むことをおすすめしたい。

コメント