目录
摘要
abstrct
一、HW3——食物图片分类CNN
二、GAN计算推导
1、引入
2、最大似然估计
3、divergence的计算
4、总结
三、GAN的架构——fGAN
1、f-divergence
2、共轭函数
3、connetction with GAN
总结
摘要
本周进一步学习了GAN基本原理,主要以GAN的计算推导和fGAN为主,回顾了最大似然函数和JS-divergence。并且动手实践了李宏毅机器学习的作业3,从数据集的处理、模型及其参数的定义以及训练和测试数据集等各方面进行了详细的代码分析。
abstrct
This week, we further studied GAN fundamentals, mainly focusing on the computational derivation of GAN and fGAN, reviewing the maximum likelihood function and JS-divergence. we also practiced Hongyi Li's Machine Learning Assignment 3, which provides a detailed code analysis of various aspects, such as the processing of datasets, the definition of the model and its parameters, as well as the training and testing dataset.
一、HW3——食物图片分类CNN
数据集的处理
class FoodDataset(Dataset):
def __init__(self,path,tfm=test_tfm,files = None):
super(FoodDataset).__init__()
self.path = path
self.files = sorted([os.path.join(path,x) for x in os.listdir(path) if x.endswith(".jpg")])
if files != None:
self.files = files
self.transform = tfm
def __len__(self):
return len(self.files)
def __getitem__(self,idx):
fname = self.files[idx]
im = Image.open(fname)
im = self.transform(im)
try:
label = int(fname.split("/")[-1].split("_")[0])
except:
label = -1 # test has no label
return im,label
初始化方法:
path——数据集的路径
files——加载数据集的文件列表
tfm——数据增强
__len__方法:
返回数据集中的图像数量
__getitem__方法:
返回图像及其标签(做完数据增强)
数据增强
test_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])
train_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])
ToTensor()是最后一行执行的,旨在于将数据类型转化为tensor张量。其他数据增强的方法有翻转、旋转、缩放、平移、裁剪、颜色变换、噪声添加、模糊和仿射变换等,目的是为了提高模型的泛化能力,解决了数据少的问题并且有效防止了过拟合的问题。
模型的定义——CNN分类
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()# torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1), # [64, 128, 128]
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [64, 64, 64]
nn.Conv2d(64, 128, 3, 1, 1), # [128, 64, 64]
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [128, 32, 32]
nn.Conv2d(128, 256, 3, 1, 1), # [256, 32, 32]
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [256, 16, 16]
nn.Conv2d(256, 512, 3, 1, 1), # [512, 16, 16]
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [512, 8, 8]
nn.Conv2d(512, 512, 3, 1, 1), # [512, 8, 8]
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [512, 4, 4]
)
self.fc = nn.Sequential(
nn.Linear(512*4*4, 1024),
nn.ReLU(),
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, 11)
)
def forward(self, x):
out = self.cnn(x)
out = out.view(out.size()[0], -1)
return self.fc(out)
该模型含有5个卷积层和1个全连接层。”卷积+归一化+激活函数+最大池化“作为一个整体构成一层卷积层,全连接层将输出展开成一行做线性变换
训练模型参数的设置
device = "cuda" if torch.cuda.is_available() else "cpu"
model = Classifier().to(device)
batch_size = 64
n_epochs = 8
patience = 5 #早停机制:为了有效防止过拟合
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
设置训练参数的值,并且初始化模型
早停机制:一种机器学习模型调优策略,提升调优效率。是一种正则化形式,用于在使用梯度下降等迭代法训练学习器时避免过拟合。这种方法会更新学习器,使其每次迭代都能更好地适应训练数据。参数是patience
加载数据集
train_set = FoodDataset("/kaggle/input/dataset/food-11/training", tfm=train_tfm)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
valid_set = FoodDataset("/kaggle/input/dataset/food-11/validation", tfm=test_tfm)
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
训练前,加载训练集和验证集并且分别进行数据增强,设置好其参数
开始训练
#初始化
stale = 0
best_acc = 0
for epoch in range(n_epochs):
#开始训练训练集
model.train()
#记录损失函数和精确度
train_loss = []
train_accs = []
for batch in tqdm(train_loader):
imgs, labels = batch #一组图像数据及其所对应的标签是一个batch
#imgs = imgs.half()
#print(imgs.shape,labels.shape)
logits = model(imgs.to(device)) #模型在最后一层输出的原始预测值
loss = criterion(logits, labels.to(device)) #采用交叉熵来计算损失
acc = (logits.argmax(dim=1) == labels.to(device)).float().mean() #计算当前batch的准确度
optimizer.zero_grad() #梯度清零
loss.backward() #计算梯度下降的参数
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10) #裁剪梯度范数:防止梯度爆炸
optimizer.step() #更新梯度参数
#记录损失和精确度
train_loss.append(loss.item())
train_accs.append(acc)
#计算损失和精确度的均值
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
print(f"[ Train | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
#开始训练验证集
model.eval()
valid_loss = []
valid_accs = []
for batch in tqdm(valid_loader):
imgs, labels = batch
with torch.no_grad(): #验证集的迭代不需要梯度下降
logits = model(imgs.to(device))
loss = criterion(logits, labels.to(device)) #交叉熵计算损失
acc = (logits.argmax(dim=1) == labels.to(device)).float().mean() #计算精确度
# 记录损失和精确度
valid_loss.append(loss.item())
valid_accs.append(acc)
#计算损失和精确度的均值
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)
print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 以当前验证集上的表现来更新日志
if valid_acc > best_acc:
with open(f"./{_exp_name}_log.txt","a"):
print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> best")
else:
with open(f"./{_exp_name}_log.txt","a"):
print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 如果说验证集的表现更佳,那么将将该准确度替代当前记录中的准确度
if valid_acc > best_acc:
print(f"Best model found at epoch {epoch}, saving model")
torch.save(model.state_dict(), f"{_exp_name}_best.ckpt") # only save best to prevent output memory exceed error
best_acc = valid_acc
stale = 0
else:
stale += 1
if stale > patience:
print(f"No improvment {patience} consecutive epochs, early stopping")
break
训练的数据集分为训练集和验证集,它们的基本操作都是不停地迭代各个batch得到训练结果,接着将训练结果与真实标签做交叉熵损失和精确度计算。但是二者有一定的区别,训练集需要进行梯度下降,而验证集不需要。
以下是更新日志中的精确度的两种情况:
case1:第六轮训练中最终更新的精确度是0.57340,验证集并没有比它更优,所以不进行log中acc的跟新
case2:第四轮训练中最终跟新的准确度是0.55971,最后第六轮训练验证集比其更优,所以对log中的acc进行更新
测试训练效果并保存
#测试集数据加载
set = FoodDataset("/kaggle/input/dataset/food-11/testing", tfm=test_tfm)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)
#测试集放入模型中预测
model_best = Classifier().to(device) #初始化模型并加载至最佳状态
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt")) #加载之前保存的最佳模型状态字典
model_best.eval() #将模型设置为评估模式
prediction = [] #预测结果列表,包含每个预测中概率最高的类别索引
with torch.no_grad():
for data,_ in tqdm(test_loader):
test_pred = model_best(data.to(device)) #测试数据放入最佳模型中进行预测
test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1) #获取每个预测中概率最高的类别的索引
prediction += test_label.squeeze().tolist() #减少预测结果的维度def
#创建一个csv文件用来保存测试集的预测结果
pad4(i): #为了保证序号的一致,不足4位的左补零
return "0"*(4-len(str(i)))+str(i)
df = pd.DataFrame()
df["Id"] = [pad4(i) for i in range(len(test_set))] #id是测试集中的图片序号,category是预测结果
df["Category"] = prediction
df.to_csv("submission.csv",index = False)
与前面训练数据集相同,测试数据集也需要加载测试集并将其放入训练出的最好的模型中。最终得到测试集的预测结果,结果如下:
可视化数据增强
import torchvision.transforms as transforms
# 定义一个包含多种数据增强技术的train_tfm
train_tfm = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机调整大小并裁剪到224x224
transforms.RandomHorizontalFlip(p=0.5), # 以0.5的概率随机水平翻转
transforms.RandomVerticalFlip(p=0.5), # 以0.5的概率随机垂直翻转
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 随机调整亮度、对比度、饱和度和色调
transforms.RandomRotation(degrees=15), # 随机旋转最多15度
transforms.ToTensor(), # 将图像转换为张量
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 使用均值和标准差进行标准化
])
# 加载图像
image_path = '/kaggle/input/dataset/food-11/training/0_0.jpg' # 替换为你的图像路径
image = Image.open(image_path)
# 应用数据增强并可视化
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 10))
for i, ax in enumerate(axes.flat):
# 应用数据增强
augmented_image = train_tfm(image)
# 反标准化以便可视化
mean = torch.tensor([0.485, 0.456, 0.406])
std = torch.tensor([0.229, 0.224, 0.225])
augmented_image_unnormalized = augmented_image * std[:, None, None] + mean[:, None, None]
# 转换为PIL图像以便显示
augmented_image_pil = transforms.ToPILImage()(augmented_image_unnormalized)
# 显示图像
ax.imshow(augmented_image_pil)
ax.axis('off')
ax.set_title(f'Augmented {i+1}')
plt.tight_layout()
plt.show()
可视化结果如下:
上面一共进行了6次迭代,代表了原图像经过了6次由随机参数构成的数据增强变换,每一次都需要进行train_tfm中的7个transform
最终的输出
二、GAN计算推导
1、引入
前两周对GAN概念的理解可以看出,GAN主要就是将一组generator和discriminator不断优化的过程,二者的目标相互对立,但是可以促进对方的改进。 generator主要负责生成与真实图片相近的图片,discriminator主要负责判别真实图片和生成图片。GAN最终的目的就是生成比较像真实图片的图片,越像越好。
假设和分别代表生成数据的分布和真实数据的分布,是指两个分布之间的距离,”最优generator“是取得该距离最小值时的,公式如下:
问题:如何判断和的分布最相似
解决:最大似然估计
2、最大似然估计
step1 给定真实数据分布
step2 假设生成数据分布,带有参数,可以调整生成更多接近的数据
step3 最大似然估计 ,其中是L取得最大值的参数
step4 计算,如下:
问题:其中和的分布都是未知的,如何计算?
解决:需要引入discriminator,判别器可以量出两个分布之间的距离
训练discriminator需要定义以上function,其中G是固定值。该训练函数类似于分类任务中的交叉熵损失函数的计算。最终需要”最优discriminator“是取得的最大值时的,因为discriminator所做的是将D和G尽可能的分开,意味着两个分布的距离取最大值。
3、divergence的计算
最终发现转换成了JS-divergence
接着再将最优discriminator带入到中去,得到如下式子:
我们需要在不同的G上,找到D曲线最大值的地方(说明discriminator达到最优解),然后比较多个最大值谁更小,选择最小的为(说明generator达到最优解)。
4、总结
二分类交叉熵
参考文章
论文GAN(生成对抗网络)的一些解读_对抗网络gan中文公式解读-CSDN博客
GAN 公式推导_生成对抗网络gan的公式-CSDN博客
三、GAN的架构——fGAN
1、f-divergence
能够发现,随着函数的变化,f-divergence也会有所不同,会出现很多种形式的divergence,如下:
2、共轭函数
共轭函数:是函数的某种对偶变换。
需要在不同的t上找到使得式子最大的x。首先固定一个t,接着不断调整x找到的最大值。要想使得计算简化,就画出每个x的函数式,在所有t的范围内找到最大值的,如下图所示:
3、connetction with GAN
由上面的共轭函数不断地推导,再带入到f-divergence中去,最终得到了的计算。如下:
总结
本周继续深入了GAN的学习,并且以代码实践来回顾了CNN的学习。下周将学习GAN中的半监督学习,并且继续动手代码实践。