2022年度第6回卒研セミナー(2022/05/26)

関連サイトと資料

演習に必要なライブラリのインストール

第2章で必要なライブラリを以下に示します。

今後、必要になるライブラリを以下に示します。

とりあえず、今回は第2章で必要なライブラリのみをインストールすることにします。


  1. 下の図の赤丸をクリックして、コマンドプロンプトを開いてください。


  2. 「conda activate torch_gpu」と入力して、エンターキーを押してください。 プロンプトの左側のカッコの中の表示が「base」から「torch_gpu」に変化し、仮想環境「torch_gpu」に入ったことを示しています。


  3. 仮想環境「torch_gpu」で、 「pip install jupyter --proxy=https://ccproxyz.kanagawa-it.ac.jp:10080」と入力して、 エンターキーを押してください。
  4. 仮想環境「torch_gpu」で、 「pip install opencv-python --proxy=https://ccproxyz.kanagawa-it.ac.jp:10080」と入力して、 エンターキーを押してください。
  5. 仮想環境「torch_gpu」で、 「pip install pandas --proxy=https://ccproxyz.kanagawa-it.ac.jp:10080」と入力して、 エンターキーを押してください。
  6. 仮想環境「torch_gpu」で、 「pip install scikit-learn --proxy=https://ccproxyz.kanagawa-it.ac.jp:10080」と入力して、 エンターキーを押してください。

Jupyter Notebookの使用方法

  1. 「ドキュメント」フォルダーの下の「Python script」フォルダーの下の「thesis」というフォルダーの下に「20220526」という作業フォルダーを作成します。
  2. メニュー「ファイル」-「フォルダを開く...」を選択し、当該作業フォルダーを開く。
  3. 下図のようなダイアログが表示された場合には、 赤丸の箇所にチェックを入れ、青い矩形で囲まれたボタンをクリックします。


  4. メニュー「表示」-「コマンドパレット...」を選択し、 表示される選択リスト上部のテキストボックスに「jupyter」と入力します。 すると、「Create New Jupyter Notebook」という項目が表示されるので、 クリックします。


  5. すると、下図のような新しいNotebookが作成されます。


  6. 右端の「カーネルの選択」をクリックし、表示される選択リストから「torch_gpu」をクリックします。



  7. 青い枠の中に、「print('hello jupyter')」と入力します。 そして、青い枠の左側にある実行ボタンをクリックします。 すると、青い枠の下に実行結果が表示されます。



  8. メニュー「ファイル」-「名前を付けて保存...」を選択し、 表示されるダイアログで「ファイルの種類」を「拡張子なし」に、「ファイル名」を「test1.ipynb」に変更し、保存ボタンをクリックする。


サンプルデータの準備

  1. ネットワークストレージに置いてある「forest-path-movie-dataset-main.zip」ファイルを、 作業フォルダー「20220526」にコピーして、解凍してください。

学習プログラム - 計算に時間がかかりすぎて(?)終わりません

新しいJupyter Notebookを作成し、カーネルを「torch_gpu」に設定します。 以下は、コードセルに入力するプログラムです。

import numpy as np
import pandas as pd
import itertools
import shutil
from PIL import Image
import os
  
import torch
from torch import nn, utils, optim
from torchvision import transforms, models
from sklearn.metrics import f1_score, accuracy_score
from sklearn.model_selection import train_test_split
  
# proxy
os.environ["http_proxy"] = "http://ccproxyz.kanagawa-it.ac.jp:10080"
os.environ["https_proxy"] = "http://ccproxyz.kanagawa-it.ac.jp:10080"
    

# GPUを使うかどうか
USE_DEVICE = 'cuda:0' if torch.cuda.is_available() else 'cpu'
# データがあるディレクトリ
INPUT_DIR = 'forest-path-movie-dataset-main/forest-path-movie-dataset-main/'
    

# PyTorchの内部を決定論的に設定する
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
  
# 乱数を初期化する関数
def init_seed():
    np.random.seed(0)
    torch.manual_seed(0)
  
# 一時ディレクトリを作成
if not os.path.isdir('tmp'):
    os.mkdir('tmp')
    

# データセットの定義ファイルを読み込む
df = pd.read_csv(INPUT_DIR+'all_file.csv')
  
# シーン毎に分割するので、groupbyして取り出す
file, person = [], []
for g in df.groupby(df.scene):
    file.append(g[1].file.values.tolist())
    person.append(g[1].person.values.tolist())
    

# シーン毎に学習用と評価用データに分ける
train_X, test_X, train_y, test_y = train_test_split(file, person, test_size=0.3, random_state=0)
    

# 全てのシーン内のデータを繋げた配列にする
train_X = sum(train_X, [])
train_y = sum(train_y, [])
test_X = sum(test_X, [])
test_y = sum(test_y, [])
    

# PyTorchの流儀でデータセットをクラスで定義する
class MyDataset:
    def __init__(self, X, y, valid=False):
        # 初期化 Xはファイル名のリスト、yは人物が写っているかどうかのリスト
        self.X = X
        self.y = y
        if not valid: # 学習用ならDAを含んだtransoformを作る
            trans = [
                transforms.Resize((224,224)),
                transforms.ColorJitter(brightness=1.0),
                transforms.RandomGrayscale(0.1),
                transforms.ToTensor(),
                transforms.RandomErasing(),
                transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225])
            ]
        else: # 評価時にはDAを含まないtransoformを作る
            trans = [
                transforms.Resize((224,224)),
                transforms.ToTensor(),
                transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                     std=[0.229, 0.224, 0.225])
            ]
        self.trans = transforms.Compose(trans)
  
    def __len__(self):
        # データセットの長さを返す
        return len(self.X)
  
    def __getitem__(self, pos):
        # posの場所にあるデータを返す
        f = INPUT_DIR + self.X[pos] # ファイルパス
        X = Image.open(f) # ファイルを読み込む
        X = self.trans(X) # DAしてtensorにする
        y = self.y[pos]
        return X, y
    

def get_model(): # ニューラルネットワークのモデルを返す関数
    # ModelZOOからモデルをダウンロードして最後の層だけを入れ替える
    model = models.resnet50(pretrained=True)
    model.fc = nn.Linear(2048, 2) # 出力の数=2にする
    model = model.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
    return model
    

def get_optim(model, lr): # 勾配降下法のアルゴリズムを返す関数
    params = model.parameters() # 学習させるパラメーター
    optimizer = optim.SGD(params, lr=lr,  momentum=0.9) # 学習率を設定
    return optimizer
    

def get_loss(weight): # 損失関数を返す関数
    # 不均衡なデータを学習させるために、クラス毎のウェイトを設定する
    weight = torch.tensor([1.0-weight,weight], dtype=torch.float)
    weight = weight.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
    loss = nn.CrossEntropyLoss(weight=weight) # ウェイト付きのCrossEntropy
    return loss
    

def get_score(true_valid, pred_valid): # 評価スコアを返す関数
    # 評価スコアは、全体の他に時間帯毎にも作成するのでディクショナリを用意する
    timezone = {'daytime':([],[]),'twilight':([],[]),'midnight':([],[])}
    # 認識結果を時間帯毎に仕分けする
    for i, filename in enumerate(test_X):
        w = df[df.file==filename].when.values[0]
        timezone[w][0].append(true_valid[i])
        timezone[w][1].append(pred_valid[i])
    # 時間帯毎のF1スコアをディクショナリに入れる
    score = {k:f1_score(v[0], v[1]) for k,v in timezone.items()}
    # 全体のF1スコアをディクショナリに入れる
    score['total'] = f1_score(true_valid, pred_valid)
    # 認識が極端に偏ってないか見るために、認識値の平均も求める
    score['average'] = np.mean(pred_valid)
    return score
    

# 学習時と評価時のバッチサイズ
BATCH_SIZE = 16
BATCH_SIZE_VALID = 4
# データの読み込みスレッドの数
NUM_WORKERS = 2
# 試行時の学習エポック数
NUM_EPOCHS = 3
# 評価で試す学習率
LR_TESTS = [1e-3,2e-4,5e-5]
# 試すウェイトは、人物の方が分散が大きいので、クラス1側を0.5より少なくする
WEIGHT_TESTS = [0.1,0.2,0.3,0.4,0.5]
    

# 学習用と評価用にデータセットを作る
train_ds = MyDataset(train_X, train_y)
test_ds = MyDataset(test_X, test_y, True)
  
# 複数スレッドでファイルを読み込みつつデータを取り出すDataLoaderを作る
data_loader = utils.data.DataLoader(
    train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS)
data_loader_v = utils.data.DataLoader(
    test_ds, batch_size=BATCH_SIZE_VALID, shuffle=False, num_workers=NUM_WORKERS)
    

# 試行時の最も評価が良かったスコアのリスト
best_scores = []
# 学習率とウェイトを変えながら試行する
for t, (lr, weight) in enumerate(itertools.product(LR_TESTS, WEIGHT_TESTS)):
    # 試行毎に乱数を初期化してからニューラルネットワークを作成する
    init_seed()
    model = get_model() # ニューラルネットワークを作成
    # 学習のためのアルゴリズムを取得
    optimizer = get_optim(model, lr)
    loss = get_loss(weight)
  
    # 現在の学習率とウェイトで試行する
    print(f'test #{t} lr={lr} weight={weight}')
    scores = [] # 各エポック終了時のスコア
  
    # 学習ループ
    for epoch in range(NUM_EPOCHS):
        total_loss = [] # 各バッチ実行時の損失値
        model.train() # モデルを学習用に設定する
        for X, y in data_loader: # 画像を読み込んでtensorにする
            X = X.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
            y = y.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
  
            # ニューラルネットワークを実行して損失値を求める
            losses = loss(model(X), y)
  
            # 新しいバッチ分の学習を行う
            optimizer.zero_grad() # 一つ前の勾配をクリア
            losses.backward() # 損失値を逆伝播させる
            optimizer.step() # 新しい勾配からパラメーターを更新する
  
            # 損失値を保存しておく
            total_loss.append(losses.detach().cpu().numpy())
  
        # 評価
        with torch.no_grad():
            # 評価時の損失値と正解/認識結果を入れるリスト
            total_loss_v = []
            true_valid = []
            pred_valid = []
  
            model.eval() # モデルを推論用に設定する
            for i, (X, y) in enumerate(data_loader_v):
                X = X.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
                y = y.to(USE_DEVICE) # GPUを使うときはGPUメモリ上に乗せる
  
                res = model(X) # ニューラルネットワークの実行
                losses = loss(res, y) # 評価データの損失値
  
                # 正解データを保存
                y = y.detach().cpu().numpy() # CPUメモリに入れてnumpy化
                true_valid.extend(y.tolist())
                # 認識結果を保存
                res = res.detach().cpu().numpy() # CPUメモリに入れてnumpy化
                pred_valid.extend(res.argmax(axis=1).tolist())
  
                # 損失値を保存しておく
                total_loss_v.append(losses.detach().cpu().numpy())
  
        # エポック終了時のスコアを求める
        total_loss = np.mean(total_loss) # 各バッチの損失の平均
        total_loss_v = np.mean(total_loss_v) # 各バッチの損失の平均
        score = get_score(true_valid, pred_valid) # 評価スコア
        scores.append(score['total']) # スコアを保存しておく
        # エポック終了時のスコアを表示する
        print(f'epoch #{epoch}: train_loss:{total_loss} valid_loss:{total_loss_v} score:{score}')
        # エポック終了時のモデルを保存しておく
        torch.save(model.state_dict(), f'tmp/checkpoint{epoch}.pth')
  
    # 現在の学習率とウェイトで最も良かったモデルをコピーして保存しておく
    best_epoch = np.argmax(scores)
    shutil.copyfile(f'tmp/checkpoint{best_epoch}.pth',f'tmp/test{t}_best.pth')
    # 現在の学習率とウェイトで最も良かったモデルを損失値しておく
    best_scores.append(scores[best_epoch])
  
    # GPUメモリをGCする
    del model, optimizer, loss, X, y, res, losses
    torch.cuda.empty_cache()
    

# 最も良かった学習率とウェイトでのモデルをコピーする
best_of_best = np.argmax(best_scores)
shutil.copyfile(f'tmp/test{best_of_best}_best.pth', 'chapt02-model1.pth')
  
# 一時ディレクトリを削除
shutil.rmtree('tmp')
    

学習データの分析

設定ファイル「forest-path-movie-dataset-main/forest-path-movie-dataset-main/all_file.csv」は、 ヘッダを含めて18941行ある。 このファイルの冒頭の10行は、以下のようになっている。 1行目はヘッダになっていて、各列がどのようなデータなのかを示している。 これによると、1列目は画像ファイル名、2列目はシーン、3列目は人物の有無、4列目は撮影の時間帯である。

file,scene,person,when
scenes/scene-001/000.jpg,scene-001,0,daytime
scenes/scene-001/001.jpg,scene-001,0,daytime
scenes/scene-001/002.jpg,scene-001,0,daytime
scenes/scene-001/003.jpg,scene-001,0,daytime
scenes/scene-001/004.jpg,scene-001,0,daytime
scenes/scene-001/005.jpg,scene-001,0,daytime
scenes/scene-001/006.jpg,scene-001,0,daytime
scenes/scene-001/007.jpg,scene-001,0,daytime
scenes/scene-001/008.jpg,scene-001,0,daytime
    

学習プログラムでは、この設定ファイルを読み込んで、学習に使用する画像やその画像を入力した時の正答を決定している。 そのプロセスを理解するために、上記設定ファイルのサブセットを「sample.csv」という名前で作成した。 具体的には、sceneの列がscene-001とscene-002のデータとヘッダを含めた、452行のデータである。

import pandas as pd
  
# データセットの定義ファイルの一部を切り抜いたものを「sample.csv」を読み込む
# dfは、DataFrameの略らしい
df = pd.read_csv('sample.csv')

print(df)
    

print(df.scene)
    

for g in df.groupby(df.scene):
    print(g)
    

for g in df.groupby(df.scene):
    print(g[1])
    

for g in df.groupby(df.scene):
    print(g[1].file)
    

for g in df.groupby(df.scene):
    print(g[1].file.values)
    

for g in df.groupby(df.scene):
    print(g[1].file.values.tolist())
    

file = []
for g in df.groupby(df.scene):
    file.append(g[1].file.values.tolist())
  
print(file)
    

person = []
for g in df.groupby(df.scene):
    person.append(g[1].person.values.tolist())
  
print(person)
    

from sklearn.model_selection import train_test_split
  
train_X, test_X, train_y, test_y = train_test_split(file, person, test_size=0.3, random_state=0)
print(train_y)
print(test_y)
    

上記プログラムはおかしい。 学習用(train)と検証用(test)でシーンごとにデータの比率が、7:3になっていない。 正しくは、以下のようになるのではないか?

from sklearn.model_selection import train_test_split
  
train_X, test_X, train_y, test_y = [], [], [], []
for i in range(len(file)):
    trainX, testX, trainy, testy = train_test_split(file[i], person[i], test_size=0.3, random_state=0)
    train_X.append(trainX)
    test_X.append(testX)
    train_y.append(trainy)
    test_y.append(testy)
  
print(train_X)
print(test_X)
print(train_y)
print(test_y)
    

# 全てのシーン内のデータを繋げた配列にする
train_X = sum(train_X, [])
train_y = sum(train_y, [])
test_X = sum(test_X, [])
test_y = sum(test_y, [])
  
print(train_X)
print(test_X)
print(train_y)
print(test_y)