
「AutoEncoder」から見る機械学習の次元削減の意味
2020.08.13
目次
AutoEncoder から見る機械学習の次元削減の意味とは
「オッカムの剃刀」という言葉をご存知ですか?
「オッカムの剃刀」は、何か現象を説明する際、仮定は少ない方が無駄がなく分かりやすいというものです。
これは、14世紀のスコラ哲学者オッカムの有名な考え方です。
「オッカムの剃刀」とは?
例えば、皆さんの周りに、会話をするときに「え、その話いる?」といった話し方をする人はいませんか?
実は、先ほどの「14世紀のスコラ哲学者オッカムの有名な考え方です。」という言葉の中にも、剃刀で削ぎ落とすべき無駄な単語が含まれています。
「スコラ哲学」という単語にばかり目がいってしまい、強調したいはずの「オッカム」が霞んでしまいます。
このように、無駄のない説明の方が優れているという考え方を「オッカムの剃刀」といいます。
物理学でも用いられる「オッカムの剃刀」
物理学にも「オッカムの剃刀」は多く適用されており、物理現象は少ない変数で表されることがほとんどです。
中でも、有名なニュートンの運動方程式は驚くほど単純です。
$$F = ma$$
あれだけ単純なものにも、オッカムの剃刀という考え方が採用されていたのです。
「オッカムの剃刀」から「次元削減」へ
解析分野でも「オッカムの剃刀」は採用されています。
解析分野では、オッカムの剃刀の考え方の元、データの次元を減らす「次元削減」というものをよく行います。
スイスロール
以下は「スイスロール」と呼ばれる三次元データです。

実はこのスイスロール、三次元とはいいながらも、データの分布は偏っており、二次元に引き延ばしてしまうことができます。
三次元の時は見る角度によって、データの分布が重なっていたところがありました。
「次元削減」により、二次元では、データ同士の分布を切り離すことができていることが分かります。
そのため、新たなデータが入ってきた際に、「そのデータはどの分布に含まれるか?」といった判定がとても楽になります。
これが「次元圧縮」という手法で、解析に必要のない余分な要素を省くことができます。
「次元削減」と「機械学習」の関係
さて、次元削減について説明していきましたが、いよいよ「機械学習」との関係を見ていきたいと思います!
最も単純な削減法「主成分分析」
次元削減にも色々手法があり、最も単純な削減法は「主成分分析」です。
しかし主成分分析では、スイスロールのような非線形データ(並進、回転、拡大縮小以外の変形)に対してはうまく動作しません。
「ニューラルネットワーク」の根幹は次元削減
一方、「ニューラルネット」では、3層以上のパーセプトロンを持つならば、任意の非線形問題を解くことができます。
そのため、多くのニューラルネットワークでは、中間層のユニット数が入力に比べて小さくなっている場合がほとんどであり、入力層から中間層への次元削減をしていると言えます。
すなわち、ニューラルネットワークの根幹は次元削減であるということです。
「画像」と「次元削減」
では、画像空間における次元圧縮を学んでいきましょう!
画像空間というのは、縦×横の画像サイズだけの次元数(画素値)を持つため、一般的にかなり高次元です。
しかし、画像空間における分布というのは、かなり低次元な分布に偏っていると言われています。(多様体仮説)
そのため、次元削減することにより、画像の低次元な分布を得ることが期待できます。
そして、その画像空間の多くは非線形に分布しています。(本当はもっと高次元です。スイスロールもその一例です。)
「非線形」な空間の「次元削減」ときたら、「ニューラルネットワーク」でしょう。
そのため、画像空間の次元削減には、多くの場合ニューラルネットワークが用いられます。
「AutoEncoder」の構造
では、どのようなニューラルネットワークを作成すれば良いかを考えていきましょう!
まずは、内容をシンプルにするために、CNN でない「普通のニューラルネットワーク」の場合を考えていきます!
入力は縦×横の画素数だけのユニット数を持ちます。
次元削減をしたいので、ユニット数を中間層では入力層より減らしていきます。
この中間層のユニット数が、最終的に次元圧縮した次元数となります。
画像の「多様体仮説」を思い出す
ですが、これだけではニューラルネットワークは「学習」が出来ません。
ここで、画像の「多様体仮説」を思い出します。
画像の分布は、低次元に分布しているということでした。
低次元な分布からでも高次元な画像を生成することは可能なはずです!
そのため、出力層は元の入力層と同じユニット数にします。
ネットワーク構造
よって、以下のようなネットワーク構造になります。
そして、学習は入力と出力が同じになるように、ニューラルネットワークのパラメータを最適化していきます。
誤差関数は、二乗誤差で問題ないでしょう。
これが俗に言う、「自己符号化器」または「AutoEncoder」です。
実験に用いたネットワーク構造
今回使ったモデルは、pytorch 公式のサンプルを改良し、「BatchNormlization」や「LeakyLeRU」を加えた CNN 構造を取ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | #ネットワーク定義 class autoencoder(nn.Module): def __init__(self): super(autoencoder, self).__init__() self.encoder = nn.Sequential( nn.Conv2d(1, 16, 3, stride=3, padding=1), # b, 16, 10, 10 nn.LeakyReLU(0,True), nn.BatchNorm2d(16), nn.MaxPool2d(2, stride=2), # b, 16, 5, 5 nn.Conv2d(16, 32, 3, stride=2, padding=1), # b, 32, 3, 3 nn.LeakyReLU(0,True), nn.BatchNorm2d(32), nn.MaxPool2d(2, stride=1), # b, 32, 2, 2 nn.Conv2d(32, 64, 2, stride=1, padding=0), # b, 64, 1, 1 nn.LeakyReLU(0,True), nn.BatchNorm2d(64), ) self.fc1 = nn.Sequential( nn.Linear(64,latent_dim), nn.Tanh() #潜在変数を[-1,1]にするためにハイパボリックタンジェントを活性化関数に ) self.fc2 = nn.Sequential( nn.Linear(latent_dim,64), nn.LeakyReLU(0,True) ) self.decoder = nn.Sequential( nn.ConvTranspose2d(64, 32, 2, stride=1), # b, 32, 2, 2 nn.LeakyReLU(0,True), nn.BatchNorm2d(32), nn.ConvTranspose2d(32, 16, 3, stride=2), # b, 16, 5, 5 nn.LeakyReLU(0,True), nn.BatchNorm2d(16), nn.ConvTranspose2d(16, 8, 5, stride=3, padding=1), # b, 8, 15, 15 nn.LeakyReLU(0,True), nn.BatchNorm2d(8), nn.ConvTranspose2d(8, 1, 2, stride=2, padding=1), # b, 1, 28, 28 nn.Tanh() ) def forward(self, x): # ===================encoder===================== # xは元画像 latent = self.encoder(x) #CNNから得られた特徴量 latent = latent.view(-1,64) #特徴量をベクトル化 latent = self.fc1(latent) #CNNから得られた特徴量をさらに低次元の特徴に写像 pre_latent = latent #次元削減により得られた潜在変数 # ===================decoder===================== latent = self.fc2(latent) latent = latent.view(-1,64,1,1) x = self.decoder(latent) #再構成画像 return x,pre_latent |
実験結果
二次元まで圧縮した例
以下は、実際に数字画像を二次元まで次元削減した時の、再構成画像の例です。
元の画像

再構成した画像

そもそも生成できていない数字が存在したり間違えているものも多数あります。
そのため、これらの数字の分布を確認してみます。
数字の分布

「9」と「4」、「7」の分布が完全にかぶっています。
特に「4」は、そもそも生成できていないことが分かります。
他にも「3」と「5」と「8」も同様で、うまくこれらの分布を離すことができず、誤生成につながったと考えることができます。
つまり、今回のネットワークモデルでは、m-nist を二次元で埋め込むことに失敗しました。
そこで三次元でも、これらの分布を可視化してみます。
三次元まで圧縮した例
まずは、再構成前後の画像例をご覧ください。
元の画像

再構成した画像

二次元の時と比べて、表現できる数字の数が増えたのが、ぱっと見で分かるかと思います。
ただ、「9」と「7」の組み合わせや、「4」と「9」等に間違いが多いことが分かります。
実際に、これらの数字の分布が近いかどうかを見てみます。
数字の分布


確かにこれらの分布は近く、特に「9」関連で間違いが多そうな気がします。
また、見方によっては分離できていないように見えていても、違う角度から見ると分離できている「3」と「5」のような組み合わせもあり、大変興味深いです。
三次元にまで削減すれば、各数字のある程度の分布を分けることができました。
さいごに
今回は、手書き文字を用いた「AutoEncoder」を利用して、「次元圧縮」について解説してみました!
削減した次元が何を表しているのかは、自分で解釈する必要があります。
「曲がり具合」を表しているのか、それとも「線の太さ」を表しているのか?
これは、「主成分分析」であっても同じです。
これがデータサイエンスの難しい所です。
次元は減らし過ぎもよくない
また、次元数は減らし過ぎてもいけません。
こればかりは、試してみて一番良いユニット数を自分たちで見つけるしかありません。
ここがニューラルネットワークの弱点でもあります。
次回は、オートエンコーダと深い関係のある、「VAE」という手法をお話ししていきます。
(株)ライトコードは、WEB・アプリ・ゲーム開発に強い、「好きを仕事にするエンジニア集団」です。
機械学習でのシステム開発依頼・お見積もりはこちらまでお願いします。
また、機械学習系エンジニアを積極採用中です!詳しくはこちらをご覧ください。
※現在、多数のお問合せを頂いており、返信に、多少お時間を頂く場合がございます。
こちらの記事もオススメ!
全体のソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | import torch import torchvision import torchvision.datasets as dset from torch import nn from torch.autograd import Variable from torch.utils.data import DataLoader from torchvision import transforms from torchvision.utils import save_image from mpl_toolkits.mplot3d import axes3d from torchvision.datasets import MNIST import os import pylab import matplotlib.pyplot as plt #カレントディレクトリにdc_imgというフォルダが作られる if not os.path.exists('./dc_img'): os.mkdir('./dc_img') #カレントディレクトリにdataというフォルダが作られる if not os.path.exists('./data'): os.mkdir('./data') num_epochs = 100 #エポック数 batch_size = 100 #バッチサイズ learning_rate = 1e-2 #学習率 train = True #Trueなら訓練用データ、Falseなら検証用データを使う pretrained = False #学習済みのモデルを使うときはここをTrueに latent_dim = 3 #最終的に落とし込む次元数 save_img = False #元画像と再構成画像を保存するかどうか、バッチサイズが大きいときは保存しない方がいい def to_img(x): x = 0.5 * (x + 1) x = x.clamp(0, 1) x = x.view(x.size(0), 1, 28, 28) return x #画像データを前処理する関数 transform = transforms.Compose( [transforms.ToTensor(), transforms.Normalize((0.5, ), (0.5, ))]) #このコードで自動で./data/以下にm-nistデータがダウンロードされる trainset = torchvision.datasets.MNIST(root='./data/', train=True, download=True, transform=transform) #このコードで自動で./data/以下にm-nistデータがダウンロードされる testset = torchvision.datasets.MNIST(root='./data/', train=False, download=True, transform=transform) #学習時なら訓練用データを用いる if train: dataloader = DataLoader(trainset, batch_size=batch_size, shuffle=True) #テスト時なら検証用データを用いる else: dataloader = DataLoader(testset, batch_size=batch_size, shuffle=True) #ネットワーク定義 class autoencoder(nn.Module): def __init__(self): super(autoencoder, self).__init__() self.encoder = nn.Sequential( nn.Conv2d(1, 16, 3, stride=3, padding=1), # b, 16, 10, 10 nn.LeakyReLU(0,True), nn.BatchNorm2d(16), nn.MaxPool2d(2, stride=2), # b, 16, 5, 5 nn.Conv2d(16, 32, 3, stride=2, padding=1), # b, 32, 3, 3 nn.LeakyReLU(0,True), nn.BatchNorm2d(32), nn.MaxPool2d(2, stride=1), # b, 32, 2, 2 nn.Conv2d(32, 64, 2, stride=1, padding=0), # b, 64, 1, 1 nn.LeakyReLU(0,True), nn.BatchNorm2d(64), ) self.fc1 = nn.Sequential( nn.Linear(64,latent_dim), nn.Tanh() #潜在変数を[-1,1]にするためにハイパボリックタンジェントを活性化関数に ) self.fc2 = nn.Sequential( nn.Linear(latent_dim,64), nn.LeakyReLU(0,True) ) self.decoder = nn.Sequential( nn.ConvTranspose2d(64, 32, 2, stride=1), # b, 32, 2, 2 nn.LeakyReLU(0,True), nn.BatchNorm2d(32), nn.ConvTranspose2d(32, 16, 3, stride=2), # b, 16, 5, 5 nn.LeakyReLU(0,True), nn.BatchNorm2d(16), nn.ConvTranspose2d(16, 8, 5, stride=3, padding=1), # b, 8, 15, 15 nn.LeakyReLU(0,True), nn.BatchNorm2d(8), nn.ConvTranspose2d(8, 1, 2, stride=2, padding=1), # b, 1, 28, 28 nn.Tanh() ) def forward(self, x): # ===================encoder===================== # xは元画像 latent = self.encoder(x) #CNNから得られた特徴量 latent = latent.view(-1,64) #特徴量をベクトル化 latent = self.fc1(latent) #CNNから得られた特徴量をさらに低次元の特徴に写像 pre_latent = latent #次元削減により得られた潜在変数 # ===================decoder===================== latent = self.fc2(latent) latent = latent.view(-1,64,1,1) x = self.decoder(latent) #再構成画像 return x,pre_latent def main(): #gpuデバイスがあるならgpuを使う #ないならcpuで device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") #ネットワーク宣言 model = autoencoder().to(device) #事前に学習したモデルがあるならそれを使う if pretrained: param = torch.load('./conv_autoencoder_{}dim.pth'.format(latent_dim)) model.load_state_dict(param) #誤差関数は二乗誤差で criterion = nn.MSELoss() #最適化法はAdamを選択 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) for epoch in range(num_epochs): print(epoch) for data in dataloader: img, num = data #img --> [batch_size,1,28,28] #num --> [batch_size,1] #imgは画像本体 #numは画像に対する正解ラベル #ただし、学習時にnumは使わない #imgをデバイスに乗っける img = Variable(img).to(device) # ===================forward===================== #outputが再構成画像、latentは次元削減されたデータ output,latent = model(img) #学習時であれば、ネットワークパラメータを更新 if train: #lossを計算 #元画像と再構成後の画像が近づくように学習 loss = criterion(output, img) # ===================backward==================== #勾配を初期化 optimizer.zero_grad() #微分値を求める loss.backward() #パラメータの更新 optimizer.step() print('{}'.format(loss)) # ===================log======================== #データをtorchからnumpyに変換 z = latent.cpu().detach().numpy() num = num.cpu().detach().numpy() #次元数が3の時のプロット if latent_dim == 3: fig = plt.figure(figsize=(15, 15)) ax = fig.add_subplot(111, projection='3d') ax.scatter(z[:, 0], z[:, 1], z[:, 2], marker='.', c=num, cmap=pylab.cm.jet) for angle in range(0,360,60): ax.view_init(30,angle) plt.savefig("./fig{}.png".format(angle)) #次元数が2の時のプロット if latent_dim == 2: plt.figure(figsize=(15, 15)) plt.scatter(z[:, 0], z[:, 1], marker='.', c=num, cmap=pylab.cm.jet) plt.colorbar() plt.grid() plt.savefig("./fig.png") #元画像と再構成後の画像を保存するなら if save_img: pic = to_img(img.cpu().data) save_image(pic, './dc_img/real_image_{}.png'.format(epoch)) #元画像の保存 pic = to_img(output.cpu().data) save_image(pic, './dc_img/image_{}.png'.format(epoch)) #再構成後の画像の保存 #もし学習時ならモデルを保存 #バージョン管理は各々で if train == True: torch.save(model.state_dict(), './conv_autoencoder_{}dim.pth'.format(latent_dim)) if __name__ == '__main__': main() |
ライトコードよりお知らせ






一緒に働いてくれる仲間を募集しております!
ライトコードでは、仲間を募集しております!
当社のモットーは「好きなことを仕事にするエンジニア集団」「エンジニアによるエンジニアのための会社」。エンジニアであるあなたの「やってみたいこと」を全力で応援する会社です。
また、ライトコードは現在、急成長中!だからこそ、あなたにお任せしたいやりがいのあるお仕事は沢山あります。「コアメンバー」として活躍してくれる、あなたからのご応募をお待ちしております!
なお、ご応募の前に、「話しだけ聞いてみたい」「社内の雰囲気を知りたい」という方はこちらをご覧ください。
ライトコードでは一緒に働いていただける方を募集しております!
採用情報はこちら書いた人はこんな人

IT技術2021.03.02TypeScriptの型を問題形式で学べる「type-challenges」とは?
IT技術2021.03.01シスコルータのコンフィグ作成をPythonで自動化してみた!
IT技術2021.02.23【Unity】ARFoundation入門~機能解説から平面検知の実装まで~
IT技術2021.02.22Swiftでguardを使うメリットと使い方をご紹介!