庚午里藻の日記

見た映画とかアニメの備忘録にしたり、パソコンいじったことのメモにしたり

GANを試すときに独自データセットを使いたい!!!

はじめに

GANを試したいなっていうことがあると思います。でもせっかく試すならライブラリからダウンロードできるようなものではなくて実際に自分でスクレイピングしてきたもので試したいということもあると思います。あったし。

なので、今回はさらっとGANに触れた上で独自データセットを使って学習した話を書きたいと思います。

ちなみに使ったのはpytorchです。

GANについて

GANは敵対的生成ネットワークの略で、名前の通りデータを生成するネットワークです。ネットワークには生成器識別器が存在します。生成器はノイズを入力にして偽のデータを出力するネットワークです。一方、識別器生成器から出力された偽のデータと学習データを入力として、どちらが本物のデータかを識別するネットワークです。生成器は識別器を騙せるような偽のデータを生成するように訓練され、識別器は生成器が出力したデータをきちんと区別できるように訓練された結果、最終的にはいい感じのデータを生成できるようになっている、みたいな認識であっていると思います。

DCGAN

Deep Convolutional GANsでDCGANらしい。学習が安定するには追加でこういうことに気をつけた方がいいよ、みたいなことが提示されています。具体的には以下のようなことが提示されていました。

  • プーリング層を使うのではなく、畳み込み層のストライドを用いる
  • 隠れ層で全結合しない
  • バッチノーマライゼーションをする
  • 生成器では活性化関数にReLUを用いる。出力層ではTanhを用いる
  • 識別器では活性化関数にLeaklyReLUを用いる

今回拾ってきて使っているのはLSGANというGANみたいです。DCGANに加えて損失関数が二乗誤差損失なのが特徴らしい。

GANのコードに関してはこの本の6.3章のLSGANのサンプルコードをほぼそのまま使っています。

githubも公開されています。

pytorchにおけるデータセットの作り方

正直これとか見た方が早いんですけど、せっかくなんで自分でも書きます。

自分で書いたデータセットがこんな感じ。アルトリアとかジャンヌとか書いてあるのは後述のネタのせいなんでとりあえず気にしないでください。

class CustomDataset(torch.utils.data.Dataset):
    classes = ['altria', 'jeanne', 'nero', 'okita']

    def __init__(self, root, transform=None):
        #指定する場合は前処理クラスを受け取る
        self.transform = transform
        #画像とラベルの一覧を保持するリスト
        self.images = []
        self.labels = []
  
        #画像を読み込むファイルパスを取得
        altria_path = os.path.join(root, 'altria')
        jeanne_path = os.path.join(root, 'jeanne')
        nero_path = os.path.join(root, 'nero')
        okita_path = os.path.join(root, 'okita')

        #画像の一覧を取得
        altria_images = os.listdir(altria_path)
        jeanne_images = os.listdir(jeanne_path)
        nero_images = os.listdir(nero_path)
        okita_images = os.listdir(okita_path)

        altria_labels = [0] * len(altria_images)
        jeanne_labels = [0] * len(jeanne_images)
        nero_labels = [0] * len(nero_images)
        okita_labels = [0] * len(okita_images)


        #1個のリストにする
        for image, label in zip(altria_images, altria_labels):
            self.images.append(os.path.join(altria_path, image))
            self.labels.append(label)
        for image, label in zip(jeanne_images, jeanne_labels):
            self.images.append(os.path.join(jeanne_path, image))
            self.labels.append(label)
        for image, label in zip(nero_images, nero_labels):
            self.images.append(os.path.join(nero_path, image))
            self.labels.append(label)
        for image, label in zip(okita_images, okita_labels):
            self.images.append(os.path.join(okita_path, image))
            self.labels.append(label)

    def __getitem__(self, index):
        #インデックスをもとに画像のファイルパスとラベルを取得
        image = self.images[index]
        label = self.labels[index]
        #画像ファイルパスから画像を読み込む
        with open(image, 'rb') as f:
            image = Image.open(f)
            image = image.convert('RGB')
        #前処理がある場合は前処理を入れる
        if self.transform is not None:
            image = self.transform(image)
        #画像とラベルのペアを返却
        return image, label

    def __len__(self):
        #データ数を指定
        return len(self.images)

クラス内に__init__, __getitem__, __len__を入れる必要があります。画像の取得にはPILを使用しています。__init__で読み込む画像のディレクトリの情報を取得して、__getitem__ではimageとlabelを返すイメージです。__len__ではデータ数を取得します。

今回やったこと

pytorchに組み込まれていたデータセットではなく、独自データセットで試すことが目的なので、せっかくなのでネタに走ります。

今回はアルトリア顔を集めて学習したらちゃんとアルトリア顔を生成できるのかというテーマでデータを集めました。

スクレイピング

これをそのまま使わせていただきました。今回は「アルトリアペンドラゴン」、「ジャンヌダルクfate」、「沖田総司fate」、「ネロクラウディウス」を検索ワードとして上限1000枚を取得しました。結果的に以下のような感じのディレクトリができることになります。 f:id:kanoeuma_310mo:20190907205639p:plain:w300

スクレイピングした画像からいい感じにデータセットを作る

これはかなりやりたいこと依存なんですけど、とりあえず自分のやったことに沿って書きます。

取得した画像はサイズや写っているものがまちまちなので、自分のやりたいことに沿って編集する必要があります。今回の場合だと「顔のイラストのみが欲しい」、「リサイズしてサイズを揃えたい」ので顔を検出した上でそこに合わせてリサイズするコードを書きます。

今回イラストの顔検出が必要なのでちょっとググったらこんなのがありました。ねっとすごい。

この特徴量を使って以下のようなコードを書きました。

import cv2
import sys
import os

#入力は python face_extract.py (dataディレクトリ以下の対象サブディレクトリ名) (face_dataディレクトリ以下に保存したいサブディレクトリ名)

face_cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')

#ディレクトリがなかったら作成
if not os.path.exists('face_data'):
    os.mkdir('face_data')

mk_dir = os.path.join('face_data', sys.argv[2])
if not os.path.exists(mk_dir):
    os.mkdir(mk_dir)

#対象のディレクトリからデータを取得
target_dir = os.path.join('data', sys.argv[1])
file_list = os.listdir(target_dir)

for file in file_list:
    target_file = os.path.join(target_dir, file)
    print(target_file)
    img = cv2.imread(target_file)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    x = 0
    y = 0
    w = 0
    h = 0

    faces = face_cascade.detectMultiScale(gray)
    for (x, y, w, h) in faces:
        # 顔画像(グレースケール)
        roi_gray = gray[y:y+h, x:x+w]
        # 顔画像(カラースケール)
        roi_color = img[y:y+h, x:x+w]

    #画像を認識できていたら保存
    if h > 0 and w > 0:
        #リサイズ
        resize_img = cv2.resize(roi_color, (96, 96))

        cv2.imwrite(os.path.join(mk_dir, file), resize_img)

イラストの顔検出をした上で96×96にリサイズしています。第一引数にdataディレクトリ以下のディレクトリ名を、第二引数に新しく作るディレクトリ名を入力します。face_dataディレクトリを作成するとこんな感じになります。

f:id:kanoeuma_310mo:20190907211939p:plain:w300

このディレクトリをさっき示した独自データセットを作るプログラムに入れて学習を行います。

結果

全体で集められたデータセットは1000枚くらいで20000エポックぐらい回してみました。結果は以下の通り。

f:id:kanoeuma_310mo:20190907213338p:plain
100エポック

f:id:kanoeuma_310mo:20190907213406p:plain
11000エポック

f:id:kanoeuma_310mo:20190907213427p:plain
18500エポック

うーん微妙!最初の時点と比べていい感じに学習できてるのは確認できたんですが、途中からは画像がよくならなかった印象です。ここら辺の考察とかはもっとちゃんと勉強してからの方がいいのかもしれない。めっちゃありきたりな言い訳ですけど、やっぱりデータ数が少ないのは原因なんじゃないのかなあと思ったりします。ただ、とりあえず遊ぶにはちょうどいいんじゃないかと。

終わりに

GANはまだまだ色々種類があるので本気でやるならこれくらいじゃよくないなあという感じです。本気でやるのかは置いておいて。

とりあえず今回使ったコードは以下の場所に置いておくので、よかったら使ってみてください。

github.com