HW8-Autoencoder
Task
本次作业是利用Autoencoder
来实现检测异常图片的模型。训练过程为:输入图片X,Autoencoder
对输入的图片进行向量编码,其实是一个压缩过程,然后通过Decoder
将生成的向量输出为图片Y(解压),loss
即为图片Y与图片X的差异。在测试时,将测试图片A输入训练好的模型得到图片B,然后设置一个阈值a,两个图片的差距大于a的判定为异常,表明图片A与训练集的图片不属于统一类别。
由于训练集没有标签,故该训练过程为Self-supervise
无监督训练。
只运行助教提供的
Sample Code
可以通过Sample、将Autoencoder
改为fcn
可以通过Medium,就无法提升了。找到一个通过Strong、Boss的blog链接,他是将CNN
和VAE
增加了层数以及加入了normalization & linear
层。
https://nbviewer.org/ 这个网址可以预览网页上资源的.ipynb
文件。
Sample Code
相关问题会体现在注释中。
Package installation & Downloading data
# Training progress bar
!pip install -q qqdm
!pip install kaggle
from google.colab import files
uploaded = files.upload()
!mkdir ~/.kaggle
!mv kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
!kaggle competitions download -c ml2022spring-hw8
!unzip ml2022spring-hw8.zip
助教挂在GitHub上的Data已经失效,直接运行Sample Code会出现找不到Data的问题,而Kaggle上的Data可以使用,故需要按照上面的指令,使Kaggle的Data加载到Colab。参考blog
Import packages
import random
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler, TensorDataset
import torchvision.transforms as transforms
import torch.nn.functional as F
from torch.autograd import Variable
import torchvision.models as models
from torch.optim import Adam, AdamW
from qqdm import qqdm, format_str
import pandas as pd
Loading data
train = np.load('data/trainingset.npy', allow_pickle=True)
test = np.load('data/testingset.npy', allow_pickle=True)
print(train.shape) # 100k张图片,每张图片维度为 64*64*3
print(test.shape)
Random seed
def same_seeds(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
same_seeds(48763)
Autoencoder
Models & loss
class fcn_autoencoder(nn.Module):
def __init__(self):
super(fcn_autoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(64 * 64 * 3, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 12),
nn.ReLU(),
nn.Linear(12, 3)
)
self.decoder = nn.Sequential(
nn.Linear(3, 12),
nn.ReLU(),
nn.Linear(12, 64),
nn.ReLU(),
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, 64 * 64 * 3),
nn.Tanh()
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
class conv_autoencoder(nn.Module):
def __init__(self):
super(conv_autoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Conv2d(3, 12, 4, stride=2, padding=1), # Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False)
nn.ReLU(),
nn.Conv2d(12, 24, 4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(24, 48, 4, stride=2, padding=1),
nn.ReLU(),
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(48, 24, 4, stride=2, padding=1), # 反卷积.将图形逆向卷积"还原"为大图形.
nn.ReLU(),
nn.ConvTranspose2d(24, 12, 4, stride=2, padding=1),
nn.ReLU(),
nn.ConvTranspose2d(12, 3, 4, stride=2, padding=1),
nn.Tanh(),
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
self.encoder = nn.Sequential(
nn.Conv2d(3, 12, 4, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(12, 24, 4, stride=2, padding=1),
nn.ReLU(),
)
self.enc_out_1 = nn.Sequential(
nn.Conv2d(24, 48, 4, stride=2, padding=1),
nn.ReLU(),
)
self.enc_out_2 = nn.Sequential(
nn.Conv2d(24, 48, 4, stride=2, padding=1),
nn.ReLU(),
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(48, 24, 4, stride=2, padding=1),
nn.ReLU(),
nn.ConvTranspose2d(24, 12, 4, stride=2, padding=1),
nn.ReLU(),
nn.ConvTranspose2d(12, 3, 4, stride=2, padding=1),
nn.Tanh(),
)
def encode(self, x):
h1 = self.encoder(x)
return self.enc_out_1(h1), self.enc_out_2(h1)
def reparametrize(self, mu, logvar):
std = logvar.mul(0.5).exp_() # y.mul(x):y为张量,x为张量or数字.y中每个元素乘x/ y.exp_():将y中元素做以e为底数的指数,并存储在y中. https://www.cnblogs.com/picassooo/p/12583881.html
if torch.cuda.is_available():
eps = torch.cuda.FloatTensor(std.size()).normal_() # 在标准正态分布下生成std.size()大小的tensor矩阵
else:
eps = torch.FloatTensor(std.size()).normal_()
eps = Variable(eps) # pytorch两个基本对象(张量Tensor, 变量Variable).后者可以变化进行反向传播,前者无法进行反向传播
return eps.mul(std).add_(mu)
def decode(self, z):
return self.decoder(z)
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparametrize(mu, logvar)
return self.decode(z), mu, logvar
def loss_vae(recon_x, x, mu, logvar, criterion):
"""
recon_x: generating images
x: origin images
mu: latent mean
logvar: latent log variance
"""
mse = criterion(recon_x, x)
KLD_element = mu.pow(2).add_(logvar.exp()).mul_(-1).add_(1).add_(logvar)
KLD = torch.sum(KLD_element).mul_(-0.5)
return mse + KLD
Dataset module
Module for obtaining and processing data. The transform function here normalizes image’s pixels from [0, 255] to [-1.0, 1.0].
class CustomTensorDataset(TensorDataset):
"""TensorDataset with support of transforms.
"""
def __init__(self, tensors):
self.tensors = tensors
if tensors.shape[-1] == 3:
self.tensors = tensors.permute(0, 3, 1, 2) # 维度转换 100000, 64, 64, 3 -> 100000, 3, 64, 64
self.transform = transforms.Compose([
transforms.Lambda(lambda x: x.to(torch.float32)), # self define function to transform
transforms.Lambda(lambda x: 2. * x/255. - 1.),
])
def __getitem__(self, index):
x = self.tensors[index]
if self.transform:
# mapping images to [-1.0, 1.0]
x = self.transform(x)
return x
def __len__(self):
return len(self.tensors)
Training
Configuration
# Training hyperparameters
num_epochs = 50
batch_size = 2000
learning_rate = 1e-3
# Build training dataloader
x = torch.from_numpy(train)
train_dataset = CustomTensorDataset(x)
train_sampler = RandomSampler(train_dataset) # 数据随机采样,该命令效果为将训练集数据打乱.https://blog.csdn.net/qq_50001789/article/details/128974424
train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=batch_size) # 传入自定义取样策略train_sampler,此时shuffle应为False
# Model
model_type = 'cnn' # selecting a model type from {'cnn', 'fcn', 'vae', 'resnet'}
model_classes = {'fcn': fcn_autoencoder(), 'cnn': conv_autoencoder(), 'vae': VAE()}
model = model_classes[model_type].cuda()
# Loss and optimizer
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
Training loop
best_loss = np.inf
model.train()
qqdm_train = qqdm(range(num_epochs), desc=format_str('bold', 'Description'))
for epoch in qqdm_train:
tot_loss = list()
for data in train_dataloader:
# ===================loading=====================
img = data.float().cuda()
if model_type in ['fcn']:
img = img.view(img.shape[0], -1) # 将img变为两维矩阵,第一维固定,因个数不变故第二维进而确定。
# ===================forward=====================
output = model(img)
if model_type in ['vae']:
loss = loss_vae(output[0], img, output[1], output[2], criterion)
else:
loss = criterion(output, img)
tot_loss.append(loss.item())
# ===================backward====================
optimizer.zero_grad()
loss.backward()
optimizer.step()
# ===================save_best====================
mean_loss = np.mean(tot_loss)
if mean_loss < best_loss:
best_loss = mean_loss
torch.save(model, 'best_model_{}.pt'.format(model_type))
# ===================log========================
qqdm_train.set_infos({
'epoch': f'{epoch + 1:.0f}/{num_epochs:.0f}',
'loss': f'{mean_loss:.4f}',
})
# ===================save_last========================
torch.save(model, 'last_model_{}.pt'.format(model_type))
Inference
Initialize
- dataloader
- model
- prediction file
eval_batch_size = 200
# build testing dataloader
data = torch.tensor(test, dtype=torch.float32)
test_dataset = CustomTensorDataset(data)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=eval_batch_size, num_workers=1)
eval_loss = nn.MSELoss(reduction='none')
# load trained model
checkpoint_path = f'last_model_{model_type}.pt'
model = torch.load(checkpoint_path)
model.eval()
# prediction file
out_file = 'prediction.csv'
anomality = list()
with torch.no_grad():
for i, data in enumerate(test_dataloader):
img = data.float().cuda()
if model_type in ['fcn']:
img = img.view(img.shape[0], -1)
output = model(img)
if model_type in ['vae']:
output = output[0]
if model_type in ['fcn']:
loss = eval_loss(output, img).sum(-1)
else:
loss = eval_loss(output, img).sum([1, 2, 3])
anomality.append(loss)
print(f'anomality shape: {len(anomality)}')
anomality = torch.cat(anomality, axis=0) # anomality=[loss1,loss2,...,lossn],将列表中的n个loss tensor按行拼接在一起
print(f'anomality shape: {anomality.shape}')
anomality = torch.sqrt(anomality).reshape(len(test), 1).cpu().numpy()
print(f'anomality shape: {anomality.shape}')
print(f'top-10 anomality:{anomality[0:10]}')
df = pd.DataFrame(anomality, columns=['score'])
print(df.iloc[0:8,:])
df.to_csv(out_file, index_label = 'ID')