ホーム >  機械学習 > Google Colaboratory >  【GoogleColab】ニューラルネットワークの画像生成 その2【オートエンコーダ】

投稿日:   |  最終更新日:

【GoogleColab】ニューラルネットワークの画像生成 その2【オートエンコーダ】

Google ColaboratoryPython機械学習

前回に続き、GoogleColabで画像を生成するニューラルネットワークのプログラムを実行します。画像データセットのインスタンスを生成して画像変換を行います。

オートエンコーダーの画像生成のデータセット完了

前回、オートエンコーダーで画像生成をする前準備として、学習データ(画像)の取得・準備を行いました。

前回までの作成プログラム

①PyTorchをインポートし、学習データMINISTを取得します。
学習データ(画像)の取得

②読み込んだ学習データMINISTを表示して確認します。
データセットの画像確認

③学習用データセットのクラスを作成します。
学習用データセットの準備

画像の扱い方

ニューラルネットワークでは、画像を1枚ずつ入力するのではなく、ある程度複数枚まとめて入力します。この画像集合体の1つの単位を「ミニバッチ」と呼びます。その枚数を「バッチサイズ」と呼びます。

DataLoaderの設定

オートエンコーダーで画像生成

オートエンコーダーとは、入力層と出力層が同じサイズの砂時計状のニューラルネットです。

①画像を一度、一次元配列にします。

②一次元のデータは、Encorder(青枠)を進みます。ステップするごとにデータが4096(64×64)→1024(32×32)→256(24×24)と落ちていきます。

③中間層にある次元が一番小さい部分「潜在変数」を通ります。

④潜在変数から出力層までのデコーダー(緑枠)を通ります。

⑤出力層で画像が描画されます。

入力した画像の情報は、エンコーダーを通って潜在変数を進んだ時に、データが落ちます。また再びデコーダーを使って出力層まで戻した時に入力層とデータ数を一致させるため、エンコーダーとデコーダーに「重み」を追加します。(※図では省略しています。)

なお、ニューラルネットには、「バイアス項」と呼ばれる重みと入力との掛け算をおこなった後に足し合わせる項があります。各層でバイアス項を使います。

学習方法

入力と出力における要素ごとの差の二乗値を求めて、その平均(もしくは和)が最も小さくなるような基準で行うことができます。この基準を「Mean Squared Error(平均二乗誤差) 」もしくはMES損失(MES LOSS)と呼びます。オートエンコーダでは、各中間層の出力に対してReLU関数などの非線形な活性化関数が適用されるため、通常のニューラルネットの学習と同様、誤差逆伝播法を用いて重みのパラメータを反復的に更新して求めます。MSE損失を用いて勾配の逆伝播を行うことになります。


必要なもの

必要なものはWindowsあるいはMacPCのみです。ただし、GoogleChoromeをインストールしている必要があります。GoogleColabを使うには、以下のステップが必要です。

①Googleアカウントを用意してログインする。

②Colabにアクセスし、プログラムを書き込むノートブックを作成する。

【無料】Google Colabを使ってサクッとPythonを使う【Python】

実行環境の準備

①Colabにアクセスします。

Colab

②左上の「ドライブにアクセス」をクリックします。

③「その他」の「アプリを追加」から「Colaboratory」をインストールします。

④ノートブックをひらけばpythonのコードが記述できます。


DataSubsetのインスタンス準備

DataSubsetのインスタンスを生成し、画像などの設定を行います。

IMG_MEAN, IMG_STD = (0.5, 0.5)

trans = transforms.Compose([
  transforms.Resize(64),
  transforms.ToTensor(),
  transforms.Normalize((IMG_MEAN,),(IMG_STD,)) 
])

use_classes = [1,6,9]
for c in use_classes:
  print(fmnist.classes)

# FashionMNISTのラベルなしサブセットを用意
dataset = DataSubset(orig_dataset=fmnist,
  n_each = 200, #各クラス何枚用いるか
  use_classes = [1,6,9], #加えるクラス
  transform = trans #画像読み込み変換
)
print('dataset size:',  len(dataset))

出力結果:

Trouser
Shirt
Ankle boot
dataset size: 600

解説

trans = transforms.Compose([
  transforms.Resize(64),
  transforms.ToTensor(),
  transforms.Normalize((IMG_MEAN,),(IMG_STD,)) 
])

一連の画像処理を設定します。元は解像度が28×28ピクセルの画像ですが、「transforms.Resize(64)」で64×64に拡大しています。

# FashionMNISTのラベルなしサブセットを用意
dataset = DataSubset(orig_dataset=fmnist,
  n_each = 200, #各クラス何枚用いるか
  use_classes = [1,6,9], #加えるクラス
  transform = trans #画像読み込み変換
)

DataSubsetのインスタンスを生成します。DataSubsetクラスの「__init()__」にまず値を渡します。

orig_datasetで元のデータを渡しています。

n_eachで指定した枚数だけ取り出します。

use_classesで、「shirt」(シャツ)「Trouser」(ズボン)「Ankleboot」(ブーツ)の3クラスを指定します。(元々は10クラスあります。)

transformに、読み込みたい画像処理を指定します。


DataLoaderの設定

ニューラルネットワークの学習では、画像を入力しながらパラメーターを更新して学習を進めます。

batch_size = 32 #バッチサイズ
loader = torch.utils.data.DataLoader(
    dataset, batch_size = batch_size, shuffle=True)

def tensor_imshow(img, title = None, show=True):
  """Tensorを画像として表示"""
  img = img.numpy().transpose((1,2,0))
  img = IMG_STD * img + IMG_MEAN
  plt.imshow(np.clip(img, 0, 1))
  if title is not None: plt.title(title)
  if show: plt.show()

#ミニバッチの表示
tensor_imshow(torchvision.utils.make_grid(
    next(iter(loader))
))

表示結果:

解説

batch_size = 32 #バッチサイズ
loader = torch.utils.data.DataLoader(
    dataset, batch_size = batch_size, shuffle=True)

データからミニバッチを読み込むためのクラスです。

  • 第一引数・・・どのデータから読み込むかを指定します。
  • 第二引数・・・バッチサイズを指定します。ここでは32を指定します。一度にbatch_size分だけ読み込みます。
  • 第三引数・・・この引数をTrueにすると、データセット順に読み込むのではなく、ランダムに読み込みます。
def tensor_imshow(img, title = None, show=True):
  """Tensorを画像として表示"""
  img = img.numpy().transpose((1,2,0))
  img = IMG_STD * img + IMG_MEAN
  plt.imshow(np.clip(img, 0, 1))
  if title is not None: plt.title(title)
  if show: plt.show()

ミニバッチを1回読み込んで、画像を表示させてみます。TensorをNumpyのarrayに変換し、さらに画像として表示するための関数ternsor_imshow()を用意しておきます。

#ミニバッチの表示
tensor_imshow(torchvision.utils.make_grid(
    next(iter(loader))
))

「next(iter(loader))」は、loaderをいったんイテレーターにします。その上でミニバッチを1つ読み込んでいます。これは32枚の画像ですので、torchvision.utils.make_grid()を用いでグリッド状に並べます。


ニューラルネットの準備

入力層から潜在変数へ変換するためのネットワークを「エンコーダー」と呼び、潜在変数から出力層までのネットワークを「デコーダー」と呼びます。

class Autoencoder(nn.Module):
  """"エンコーダー、デコーダー共に3層"""
  def __init__(self,img_size, nz):
    super(Autoencoder, self).__init__()
    
    input_size = img_size * img_size
    nz1 = input_size // 4
    nz2 = nz1 // 4

    self.encoder = nn.Sequential(
        nn.Linear(input_size, nz1),
        nn.ReLU(inplace=True),
        nn.Linear(nz1,nz2),
        nn.ReLU(inplace=True),
        nn.Linear(nz2,nz),
    )

    self.decoder = nn.Sequential(
        nn.Linear(nz, nz2),
        nn.ReLU(inplace=True),
        nn.Linear(nz2,nz1),
        nn.ReLU(inplace=True),
        nn.Linear(nz1,input_size),
    )
    
  def forward(self, x):
      z=self.encoder(x)
      x=self.decoder(z)
      return x

class LinearAE(nn.Module):
  """エンコーダー、デコーダー共に1層"""
  def __init__(self,img_size, nz):
    super(LinearAE, self).__init__()
    self.encoder = nn.Linear(img_size * img_size, nz)
    self.decoder = nn.Linear(nz, img_size * img_size)

  def forward(self, x):
    z=self.encoder(x)
    x=self.decoder(z)
    return x

解説

    input_size = img_size * img_size
    nz1 = input_size // 4
    nz2 = nz1 // 4

入力層および中間層の次元を設定しています。入力層は、64×64ピクセルの画像を1次元にしているため、4096次元になります。1つ目の中間層はその1/4、その次の中間層はさらに1/4とします。真ん中の中間層は、引数をnzで指定した次元数とします(後でnz=2)。

    self.encorder = nn.Sequential(
        nn.Linear(input_size, nz1),
        nn.ReLU(inplasce=True),
        nn.Linear(nz1,nz2),
        nn.ReLU(inplasce=True),
        nn.Linear(nz2,nz),
    )

エンコーダーの定義です。

    self.dencorder = nn.Sequential(
        nn.Linear(nz, nz2),
        nn.ReLU(inplasce=True),
        nn.Linear(nz2,nz1),
        nn.ReLU(inplasce=True),
        nn.Linear(nz1,input_size),
    )

デコーダーの定義です。

class LinearAE(nn.Module):
  """エンコーダー、デコーダー共に1層"""
  def __init__(self,img_size, nz):
    super(LinearAE, self).__init__()
    self.encorder = nn.Linear(img_size * img_size, nz)
    self.dencorder = nn.Linear(nz, img_size * img_size)

  def forward(self, x):
    z=self.encoder(x)
    x=self.decoder(z)
    return x

性能比較のため、エンコーダーとデコーダーを共に線形としたオートエンコーダをLinearAEとして定義しています。層の間に非線形な活性関数を挟まない場合、多層にしても表現能力は向上しません。そのため、ここではエンコーダーデコーダー共に1層です。


結果表示用の関数

def input_and_reconst(net, loader, seed=None):
  """入力画像と再構成画像をミニバッチ分表示"""
  if seed is not None:
    torch.manual_seed(seed)

  net.eval() #推論モード
  with torch.no_grad(): #推論のみ(勾配計算なし)
    imgs = next(iter(loader)) #DataLoaderからミニバッチ抽出
    imgs_in = imgs.view(-1, imgs.shape[2]*imgs.shape[3])
    outputs = net(imgs_in.to(device)) #順伝播による推論

  #入出力それぞれのグリッド画像を生成
  grid_i = torchvision.utils.make_grid(imgs)
  grid_o = torchvision.utils.make_grid(
    outputs.view(-1, 1, imgs.shape[2], imgs.shape[3]). \
      detach().cpu())
  
  #入力画像と再構成画像をミニバッチ単位で表示
  tensor_imshow(grid_i, title = 'Inputs' )
  tensor_imshow(grid_o, title = 'Reconstructed' )

解説

結果表示用の関数input_and_reconst()を用意しておきます。この関数では、引数で与えたオートエンコーダーをを使ってloaderのミニバッチをそれぞれエンコードし、再びデコードします。これによって再構成された画像を、入力画像と共に表示します。

元の画像をもとに戻す場合は、「生成」よりむしろ「再構成(復元)」(reconstruction)と呼びます。


オートエンコーダーの準備

AutoEncorderとLinerAEのインスタンスを生成し、学習前のモデルの振る舞いを確認します。

nz = 2
img_size = loader.dataset[0].size()[-1] #画像サイズ
print(f'nz: {nz}, img_size: {img_size}')

torch.manual_seed(0) #再現性のためのシード作成

ae_net = Autoencoder(img_size, nz).to(device)
linae_net = LinearAE(img_size, nz).to(device)

input_and_reconst(ae_net, loader, seed=0)
input_and_reconst(linae_net, loader, seed=0)

出力結果:


解説

nz = 2

存在変数の次元を2次元とします。

img_size = loader.dataset[0].size()[-1] #画像サイズ

元の画像(1枚分)は64×64ピクセルですが、ここで自動取得します。64×64=4096次元の情報を、たった2つの値で元の画像の多様性を表現します。

torch.manual_seed(0) #再現性のためのシード作成

インスタンスを作成すると、重みのパラメーターが初期化されます。ある程度の再現性を得るため、乱数シードを設定します。

ae_net = Autoencoder(img_size, nz).to(device)
linae_net = LinearAE(img_size, nz).to(device)

2つのオートエンコーダーのインスタンスを生成します。

input_and_reconst(ae_net, loader, seed=0)
input_and_reconst(linae_net, loader, seed=0)

結果表示用の関数で定義した関数を使って、入力画像と再構成画像を並べて表示します。


次回

工事中。


トラックバック用のURL
プロフィール

名前:イワサキ ユウタ 職業:システムエンジニア、ウェブマスター、フロントエンドエンジニア 誕生:1986年生まれ 出身:静岡県 特技:ウッドベース 略歴 20

最近の投稿
人気記事
カテゴリー
広告