Bootstrap

一个完整的VGG项目代码+注释

 霹雳吧啦Wz的个人空间_哔哩哔哩_bilibili

Model

import torch.nn as nn
import torch

model_urls = {
    'vgg11': 'https://download.pytorch.org/models/vgg11-bbd30ac9.pth',
    'vgg13': 'https://download.pytorch.org/models/vgg13-c768596a.pth',
    'vgg16': 'https://download.pytorch.org/models/vgg16-397923af.pth',
    'vgg19': 'https://download.pytorch.org/models/vgg19-dcbb9e9d.pth'
}

class Vgg_self(nn.Module):
    def __init__(self,features,num_classes=1000,init_weights=False):
        super(Vgg_self, self).__init__()
        self.features=features
        self.layer=nn.Sequential(nn.Linear(512*7*7,4096),
                                 nn.ReLU(),
                                 nn.Dropout(p=0.5),
                                 nn.Linear(4096,4096),
                                 nn.ReLU(True),
                                 nn.Dropout(p=0.5),
                                 nn.Linear(4096,num_classes)

        )

        if init_weights:
            self._initialize_weights()

    def forward(self,x):
        x=self.features(x)
        x=torch.flatten(x,start_dim=1)
        x=self.layer(x)
        return x

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m,nn.Conv2d):
                nn.init.xavier_uniform_(m.weight)
                if m.bias is not None:
                    nn.init.constant_(m.bias,0)
            elif isinstance(m,nn.Linear):
                nn.init.xavier_uniform_(m.weight)
                nn.init.constant_(m.bias,0)


    pass

def make_features(cfg:list):
    layers=[]
    in_channels=3
    for v in cfg:
        if v=="M":
            layers+=[nn.MaxPool2d(kernel_size=2,stride=2)]
        else:
            conv2d=nn.Conv2d(in_channels,v,kernel_size=3,padding=1)
            layers+=[conv2d,nn.ReLU(True)]
            in_channels=v
    return nn.Sequential(*layers)

cfgs={
    "vgg11":[64,'M',128,'M',256,256,'M',512,512,'M',512,512,'M'],
    "vgg13":[64,64,'M',128,128,'M',256,256,'M',512,512,'M',512,512,'M'],
    "vgg16":[64,64,'M',128,128,'M',256,256,256,'M',512,512,512,'M',512,512,512,'M'],
    "vgg19":[64,64,'M',128,128,'M',256,256,256,256,'M',512,512,512,512,'M',512,512,512,512,'M']

}
def vgg(model_name='vgg16',**kwargs):
    assert model_name in cfgs,"Warning: model number {} not in cfgs dict!".format(model_name)
    cfg=cfgs[model_name]

    model=Vgg_self(make_features(cfg),**kwargs)
    return model

1.train

import torch.optim as optim
import torch
import torch.nn as nn
from torchvision import transforms,datasets
import os
import json
import sys
from  tqdm import tqdm
from 自己手敲VGG import vgg


"""训练集在送入网络前,需要被读取加载,并划分为训练集和验证集,并指定图像增强方法,然后以batch size为单位分批送入网络。

(1)transforms
torchvision.transfroms实现了丰富的图像增强方法,可以对PIL Image 和 Tensor进行转化
transforms.RandomResizedCrop(224)  # 将图片随机裁剪,并resize到224*224
transforms.RandomHorizontalFlip(p=0.5)  # 将图片以0.5的概率水平翻转
transforms.CenterCrop(224)  # 从图片中心位置,以224为尺寸进行裁剪
transforms.RandomRotation() # 按角度旋转图片
transforms.ToTensor() # 将图片转化为tensor,图片将会被归一化到[0, 1],且其维度会从(H x W x C)转为(C x H x W)
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 按mean和std归一化到[-1, 1] [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]是从ImageNet数据集上得到mean和std
transforms.RandomErasing() # 在张量图像中随机选择一个矩形区域并擦除其像素
注意,有些transforms只能对PIL或tensor类型之一使用,而有些transforms即能对PIL进行变换也可以对tensor进行变换。具体参见:https://pytorch.org/vision/stable/transforms.html

        很多情况下,我们往往会使用多种增强方法,那么便可以使用transforms.Compose将多种变换方法串联组装起来,图片会依次经过transforms.Compose中的变换:
        transforms.Compose([transforms.Grayscale(1),
                    transforms.Resize([224, 224]),
                    transforms.RandomHorizontalFlip(),
                    transforms.ToTensor(),
                    transforms.Normalize([0.485], [0.229])
                   ])
                   
为了方便对训练集和数据集采用不同的增强方法,我们可以使用一个字典来保存我们要使用的增强方法,如下代码所示,定义一个data_transform字典,该字典有两个key,分别为“train”和“val”,其value为图像增强的方法。

    data_transform = {
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
        "val": transforms.Compose([transforms.Resize(256),
                                   transforms.CenterCrop(224),
                                   transforms.ToTensor(),
                                   transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}

 (2)dataset

        对于一个图片分类任务,我们用两种方式读取数据集,一种是使用pytorch的dataset类(这个类往往需要根据自己的任务进行重写),另一种是直接使用ImageFolder     
        
        # 方式一:使用重写的Dataset类
dataset = MyData(label_path,
                 image_root_path,
                 transform=data_transform["train"])
 
train_size = int(len(dataset) * 0.7)
val_size = len(dataset) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size]) # 按比例划分成训练集和测试集
 
# 方式二:使用ImageFolder
import torchvision.datasets
train_dataset = datasets.ImageFolder(root=train_root_path,
                                     transform=data_transform["train"])
val_dataset = datasets.ImageFolder(root=val_root_path,
                                     transform=data_transform["val"])
                                     

(3)DataLoader

dataset类是pytorch中表示数据集的抽象类,那么DataLoader作为一个迭代器,每次会产生一个batch size大小的数据。      

nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
 
print('Using {} dataloader workers every process'.format(nw))
 
batch_size = 32
 
train_loader = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size, shuffle=True,
                                               num_workers=nw)
 
validate_loader = torch.utils.data.DataLoader(val_dataset,
                                                  batch_size=batch_size, shuffle=False,
                                                  num_workers=nw)
 
print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))           
                                                                           
                                                                           
                                                                           
2.引入网络模型、损失函数、优化器
在对数据集的处理完成后,需要引入网络模型、定义损失函数和优化器等。

对于优化器,我们必须要传入两个参数,一个是需要更新梯度的网络参数,另一个是学习率。

我们可以直接更新网络中的所有参数:

optimiter = torch.optim.Adam(net.parameters(), lr=0.01)                
                                   
"""
##################################此训练使用的是批梯度下降,不是随机梯度下降,1个epoch就是所有训练或者测试样本
def main():
    # device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")#如果有Gpu,就用Gpu,没有就用cpu
    device=torch.device("cpu")
    print("using {} device.".format(device))#using cuda:0 device.

    data_transform={
        "train":transforms.Compose([transforms.RandomResizedCrop(224),#随机裁剪(裁剪到224*224)
                                    transforms.RandomHorizontalFlip(),# 随机翻转
                                    transforms.ToTensor(),#转化为一个tensor
                                    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))]),# 标准化处理
        "val":transforms.Compose([transforms.Resize((224,224)),
                                  transforms.ToTensor(),
                                  transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
                                  ])
    }
    cwd=os.getcwd()#C:\Users\86131\PycharmProjects\pythonProject\手敲CV经典模型\VGG
    dd="./"
    data_root=os.path.abspath(os.path.join(cwd,dd))#C:\Users\86131\PycharmProjects\pythonProject\  # get data root path(获取目录)
    image_path=os.path.join(data_root,"data_set","flower_data")#花数据集路径
    assert os.path.exists(image_path),"{} path does not exist.".format(image_path)
    # 通过定义的data_transform这个字典,传入"train"这个key,他就会返回训练集对应的数据预处理(字典还能这么用?)
    train_dataset=datasets.ImageFolder(root=os.path.join(image_path,"train"),#通过atasets.ImageFolder加载数据集
                                       transform=data_transform['train']# transform训练数据集预处理,
                                       )
    """
    代码中root=train_dir,是一个你所放数据集的绝对路径.
    train = datasets.ImageFolder(‘train’),它生成了一个对象
    1)类别 列表形式
2)种类对应数字标签 字典形式
3)每一个图像及其对应的标签 列表形式
    """

    train_num=len(train_dataset)
    flower_list=train_dataset.class_to_idx#通过此获取分类类别的名称和其所对应的索引 # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
    cla_dict=dict((val,key) for key,val in flower_list.items())

    json_str=json.dumps(cla_dict,indent=4)

    with open("class_indices.json","w") as json_file:
        json_file.write(json_str)

    batch_size=32#train_num=3306 batch_size=32 epochs=30 iteration=4
    epochs=1
    www=os.cpu_count()
    nw=min([os.cpu_count() ,batch_size if batch_size >1 else 0,8])#就是加载数据时快一些

    """num_workers是Dataloader的概念,默认值是0。是告诉DataLoader实例要使用多少个子进程进行数据加载(和CPU有关,和GPU无关)
如果num_worker设为0,意味着每一轮迭代时,dataloader不再有自主加载数据到RAM这一步骤(因为没有worker了),而是在RAM中找batch,找不到时再加载相应的batch。缺点当然是速度慢。
当num_worker不为0时,每轮到dataloader加载数据时,dataloader一次性创建num_worker个worker,并用batch_sampler将指定batch分配给指定worker,worker将它负责的batch加载进RAM。
num_worker设置得大,好处是寻batch速度快,因为下一轮迭代的batch很可能在上一轮/上上一轮…迭代时已经加载好了。坏处是内存开销大,也加重了CPU负担(worker加载数据到RAM的进程是CPU复制的嘛)。num_workers的经验设置值是自己电脑/服务器的CPU核心数,如果CPU很强、RAM也很充足,就可以设置得更大些。
num_worker小了的情况,主进程采集完最后一个worker的batch。此时需要回去采集第一个worker产生的第二个batch。如果该worker此时没有采集完,主线程会卡在这里等。(这种情况出现在,num_works数量少或者batchsize
比较小,显卡很快就计算完了,CPU对GPU供不应求。)
即,num_workers的值和模型训练快慢有关,和训练出的模型的performance无关
Detectron2的num_workers默认是4
————————————————
"""
    print("using {} dataloader workers every process".format(nw))

    train_loader=torch.utils.data.DataLoader(train_dataset,#通过dataloader加载train_dataset
                                             batch_size=batch_size,shuffle=True,#通过给定的batch_size与随机参数,随机的从一个epoch样本中回去一批一批的数据
                                             num_workers=nw)#windows系统不能设为0值

    validate_dataset=datasets.ImageFolder(root=os.path.join(image_path,"val"),
                                          transform=data_transform["val"]#transforms测试数据集预处理
                                          )

    val_num=len(validate_dataset)
    validate_loader=torch.utils.data.DataLoader(validate_dataset,
                                                 batch_size=batch_size,
                                                 shuffle=False,
                                                 num_workers=nw
                                                 )
    print("using {} image for training,{} images for validation.".format(train_num,val_num))

    model_name='vgg13'

    net=vgg(model_name=model_name,num_classes=5,init_weights=True)#传入类别5,初始化权重设为True
    #####################################模型封装好了net直接调用,net.train() net.eval()


    net.to(device)#将网络指认到指定刚刚设置的设备上(cpu或者Gpu)

    loss_function=nn.CrossEntropyLoss()#定义损失函数,使用的是多类别交叉损失函数
    pata=list(net.parameters())#查看模型参数
    optimizer=optim.Adam(net.parameters(),lr=0.0001)#优化器时给负反馈做后勤工作 定义的优化器,优化对象是所有的可训练参数(net.parameters()),
    #学习率lr=0.0002(已经经过调试,变大变小都会影响准确率
    """优化器的作用:----->使用损失函数时,调用损失函数的backward得到反向传播,反向传播求出需要调节参数对应的梯度,梯度可以使用优化器,
    对梯度的参数进行调整。达到整体降低误差的目的。

用来更新和计算影响模型训练和模型输出的网络参数,使其逼近或达到最优值,从而最小化(或最大化)损失函数。
常见的优化器:
梯度下降法GD(Gradient Descent)
批量梯度下降法 BGD(Batch Gradient Descent)
随机梯度下降法SGD(Stochastic Gradient Descent)
小批量梯度下降法 MBGD(Mini-Batch Gradient Descent)
"""
    """running_loss += loss.item()  由 loss_function(logits, labels.to(device)) 
    返回的loss类型为张量,所以需要item()取到其数值。前面已经讲过loss_function(logits, labels.to(device)) 
    返回的是每一个mini batch的平均损失,running_loss += loss.item() 就是将每个mini batch数量的样本的平均损失累加起来
    ,方便后面计算每一个样本的平均损失。可能有点拗口,但看到后面就明白了。
"""

    """rate = (step+1)/len(train_loader) 这行代码是在计算当前epoch完成了百分之多少。DataLoader按batch来投喂数据,
    那么假设训练集有1000张图片,batch_size=32,所以train_loader需要投喂1000/32=32次,也就是len(train_loader) 的值。step是
    通过enumerate获得的,它表示当前是第step个batch。因为enumerate的start=0,所以需要step+1。假如当前是第10个batch,
    那么当前epoch进行了百分之rate = 10/32。"""

    best_acc=0.0#定义最佳准确率,保存最高准确率
    save_path='./{}.pth'.format(model_name)#保存权重路径
    train_steps=len(train_loader)#104  train_num(3306)/batch_size(32)  就是iteration

    for epoch in range(epochs):#开始训练
        net.train()#通过net.train()与net.eval()方法来管理Drouout()方法,也可以管理BN层
        #使用net.train()开启Dropout()方法,使用net.eval()关闭Dropout()方法
        running_loss=0.0#统计训练过程中的实际损失
        train_bar=tqdm(train_loader,file=sys.stdout)#train_loader是迭代器  sys.stdout的形式就是print的一种默认输出格式  以上两个输出是相同的 print 默认换行
        #tqdm是 Python 进度条库,可以在 Python长循环中添加一个进度提示信息。用户只需要封装任意的迭代器,是一个快速、扩展性强的进度条工具库。
        for step,data in enumerate(train_bar):#遍历数据集
            images,labels=data#将数据分为图片和标签
            optimizer.zero_grad()#清楚历史梯度信息  这里的梯度信息不要和参数搅浑 w-=lr*梯度 清楚的只是梯度,下一轮算出新的梯度,w继续更新
            outputs=net(images.to(device))#将图片数据集
            loss=loss_function(outputs,labels.to(device))#通过loss_function计算预测值和真实值之间的损失,这里同样将labels指认到设备中
            loss.backward()#将损失反向传播到每一个节点中
            optimizer.step()#通过optimizer更新每一个节点的参数值
            running_loss+=loss.item()#将得到的损失值累加到running_loss上,后期会求平均



            train_bar.desc="train epoch[{}/{}] loss:{:.3f}".format(epoch+1,epochs,loss)#打印训练进度  desc('str'): 传入进度条的前缀

                                                                            # train epoch[1/30] loss:1.610
    # 1%|          | 1/104 [00:19<33:29, 19.51s/it]
    #################validation
    net.eval()
    acc=0.0
    """with torch.no_grad() python的上下文管理器with就不多介绍了,
       这句代码意思简单粗暴一句话就是:with wrap下的所有代码都不更新梯度 """

    with torch.no_grad():

        val_bar=tqdm(validate_loader,file=sys.stdout)

        for val_data in val_bar:
            val_images,val_labels=val_data
            outputs=net(val_images.to(device))

            predict_y=torch.max(outputs,dim=1)[1]
            """predict_y = torch.max(outputs, dim=1)[1] 这代码的意思是获得这个batch中网络的预测标签 。
                                        torch.max(outputs, dim=1)返回两个值,分别是最大值和其对应的索引,dim=1时按行返回最大索引,
                                        dim=0时按列返回最大索引。outputs是网络的输出,是一个尺寸为的张量。 1行--》dim=1"""
            acc+=torch.eq(predict_y,val_labels.to(device)).sum().item()

        """torch.eq()是对两个张量进行逐元素的比较,若相同位置的两个元素相同,则返回True;若不同,返回False。例如网络的预测值为predict_y=[2,0,0],实际的标签值为y=[2,1,0],那么torch.eq(predict_y, y)会返回[True, False, True],注意,在python中式可以对True和False进行数学运算的,True等价于1,False等价于0。那么通过.sum()对其求和,就是预测对的值,此时还是一个张量,所以需要.item()取出对应的值。注意此时的acc是当前batch的预测正确的个数,所以需要+=来得到所有batch的预测正确的个数。
val_accurate = acc / val_num 此时的acc是验证集上的所有预测正确的个数,val_num是验证集的样本个数,val_cacurate是验证集上的准确率。
"""

        val_accurate=acc/val_num
        # 100%|██████████| 1/1 [00:07<00:00,  7.73s/it]验证集进度条
        print("[epoch %d] train_loss:%.3f val_accuracy:%.3f"%
              (epoch+1,running_loss/train_steps,val_accurate))#[epoch 1] train_loss: 1.615  val_accuracy: 0.270 验证就直接验证就完了,没有轮数一说

        if val_accurate >best_acc:
            best_acc=val_accurate
            torch.save(net.state_dict(),save_path)#保存当前权重

    print("Finished Training")



    dd=4




if __name__ == '__main__':
    main()

 2.predict

import json
import os
from PIL import Image
import matplotlib.pyplot as plt
import torch
from PIL import Image
from torchvision import transforms
from 自己手敲VGG import vgg
"""
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)):
前面的(0.5,0.5,0.5) 是 R G B 三个通道上的均值, 后面(0.5, 0.5, 0.5)是三个通道的标准差,
Normalize对每个通道执行以下操作:image =(图像-平均值)/ std.
在您的情况下,参数mean,std分别以0.5和0.5的形式传递。
这将使图像在[-1,1]范围内归一化。例如,最小值0将转换为(0-0.5)/0.5=-1,最大值1将转换为1…
注意通道顺序是 R G B ,用过opencv的同学应该知道openCV读出来的图像是 BRG顺序。这两个tuple数据是用来对RGB 图像做归一化的,
如其名称 Normalize 所示这里都取0.5只是一个近似的操作,实际上其均值和方差并不是这么多,但是就这个示例而言 影响可不计。
精确值是通过分别计算R,G,B三个通道的数据算出来的,
比如你有2张图片,都是100100大小的,那么两图片的像素点共有2100*100 = 20 000 个; 那么这两张图片的
1. mean求法:
mean_R: 这20000个像素点的R值加起来,除以像素点的总数,这里是20000;mean_G 和mean_B 两个通道 的计算方法 一样的。
2. 标准差求法:
首先标准差就是开了方的方差,所以其实就是求方差,方差公式就是我们数学上的那个求方差的公式:

"""
def main():
    device=torch.device("cuda:0" if torch.cuda.is_available() else 'cpu')
    data_transform=transforms.Compose(#很多情况下,,往往会需要多种数据增强方法,那么便可以使用
        #transforms.Compose将多种变换方法串联组装起来,图片会依次经过transforms.Compose中的变换
        [transforms.Resize((224,224)),#是缩放,记住图像尺度统一为224×224时,要用transforms.Resize([224, 224]),不能写成transforms.Resize(224),
         # transforms.Resize(224)表示把图像的短边统一为224,另外一边做同样倍速缩放,不一定为224
         transforms.ToTensor(),#ToTensor()能够把灰度范围从0-255变换到0-1之间,
         # 而后面的transform.Normalize()则把0-1变换到(-1,1).具体地说,对每个通道而言,Normalize执行以下操作:
         #image=(image-mean)/std 其中mean和std分别通过(0.5,0.5,0.5)和(0.5,0.5,0.5)进行指定。
         # 原来的0-1最小值0则变成(0-0.5)/0.5=-1,而最大值1则变成(1-0.5)/0.5=1.
         transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))###转变为-1~1
         ]

    )

    img_path= 'flow_feature/tulip.jpg'
    assert os.path.exists(img_path),"file:'{}' does not exist.".format(img_path)
    img=Image.open(img_path)
    plt.imshow(img)
    img=data_transform(img)###img:(3,244,244)-->值变为[-1,1]
    """queeze()函数的功能是维度压缩。返回一个tensor(张量),其中 input 中维度大小为1的所有维都已删除。
举个例子:如果 input 的形状为 (A×1×B×C×1×D),那么返回的tensor的形状则为 (A×B×C×D)
当给定 dim 时,那么只在给定的维度(dimension)上进行压缩操作,注意给定的维度大小必须是1,否则不能进行压缩。
举个例子:如果 input 的形状为 (A×1×B),squeeze(input, dim=0)后,返回的tensor不变,因为第0维的大小为A,不是1;
squeeze(input, 1)后,返回的tensor将被压缩为 (A×B)。

unsqueeze()函数起升维的作用,参数dim表示在哪个地方加一个维度,注意dim范围在:[-input.dim() - 1, input.dim() + 1]之间,
比如输入input是一维,则dim=0时数据为行方向扩,dim=1时为列方向扩,再大错误。
"""
    """
    torch中的squeeze与unsqueeze作用是去除/添加维度为1的行

    例如,a=torch.randn(2,3)

    那么b=a.unsqueeze(0),b为(1,2,3)的矩阵

    类似的,b=a.unsqueeze(1),b为(2,1,3)的矩阵

    b=a.unsqueeze(2),b为(2,3,1)的矩阵"""

    img=torch.unsqueeze(img,dim=0)#img:(1,3,244,244)
    json_path='./class_indices.json'
    assert os.path.exists(json_path),"file:'{}'does not exist. ".format(json_path)
    with open(json_path,'r') as f:
        class_indict=json.load(f)
    #create model
    model=vgg(model_name="vgg13",num_classes=5).to(device)

    weights_path="./vgg13.pth"
    assert os.path.exists(weights_path) ,"file:'{}' does not exist.".format(weights_path)

    model.load_state_dict(torch.load(weights_path,map_location=device))

    """model.eval()
model.eval() 作用等同于 self.train(False)
简而言之,就是评估模式。而非训练模式。
在评估模式下,batchNorm层,dropout层等用于优化训练而添加的网络层会被关闭,从而使得评估时不会发生偏移。"""

    model.eval()
    with torch.no_grad():
        output=torch.squeeze(model(img.to(device))).cpu()#tensor([a,s,d,f,g])
        print(output.size())#torch.Size([5]) 5*1
        predict=torch.softmax(output,dim=0)
        """Softmax是一种数学函数,通常用于将一组任意实数转换为表示概率分布的实数。其本质上是一种归一化函数,可以将一组任意的实数值转化为在[0, 1]之间的概率值,
        因为softmax将它们转换为0到1之间的值,所以它们可以被解释为概率。如果其中一个输入很小或为负,softmax将其变为小概率,如果输入很大,
        则将其变为大概率,但它将始终保持在0到1之间。
        
        二维情况下,dim=0时,对列进行计算
        二维情况下,dim=1时,对行进行计算
        
        三维情况下:
        当 dim=0时, 是对每一维度相同位置的数值进行 softmax运算,和为1
        当 dim=1时, 是对某一维度的列进行 softmax运算,和为1
        当 dim=2时, 是对某一维度的行进行 softmax运算,和为1
        
        """
        predict_cla=torch.argmax(predict).numpy()

    print_res="class:{} prob:{:.3}".format(class_indict[str(predict_cla)],
                                           predict[predict_cla].numpy())
    plt.title(print_res)

    for i in range(len(predict)):
        print("class:{:10} prob:{:.3}".format(class_indict[str(i)],
                                              predict[i].numpy()))
    plt.show()

if __name__ == '__main__':
    main()

3.hook提取特征--》predict

import os
import shutil
import json
import os
from PIL import Image
import matplotlib.pyplot as plt
import torch
from PIL import Image
from torchvision import transforms
from 自己手敲VGG import vgg
import torch
import torch.nn as nn
import torchvision
from torchvision import models, transforms
from PIL import Image
from torchvision.models import VGG16_Weights
import json

class VGGHook:
    def __init__(self, vgg):
        self.imgs = {}
        # hook注册
        for idx in range(25):
            vgg.features[idx].register_forward_hook(self.create_hook(idx))

    def create_hook(self, idx):
        def hook(model, layer_input, layer_output):
            self.imgs[idx] = layer_output.cpu()  # [N,C,H,W], 并且此时此刻N固定为1

        return hook


if __name__ == '__main__':
    device = torch.device("cuda:0" if torch.cuda.is_available() else 'cpu')
    data_transform = transforms.Compose(  # 很多情况下,,往往会需要多种数据增强方法,那么便可以使用
        # transforms.Compose将多种变换方法串联组装起来,图片会依次经过transforms.Compose中的变换
        [transforms.Resize((224, 224)),
         # 是缩放,记住图像尺度统一为224×224时,要用transforms.Resize([224, 224]),不能写成transforms.Resize(224),
         # transforms.Resize(224)表示把图像的短边统一为224,另外一边做同样倍速缩放,不一定为224
         transforms.ToTensor(),  # ToTensor()能够把灰度范围从0-255变换到0-1之间,
         # 而后面的transform.Normalize()则把0-1变换到(-1,1).具体地说,对每个通道而言,Normalize执行以下操作:
         # image=(image-mean)/std 其中mean和std分别通过(0.5,0.5,0.5)和(0.5,0.5,0.5)进行指定。
         # 原来的0-1最小值0则变成(0-0.5)/0.5=-1,而最大值1则变成(1-0.5)/0.5=1.
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  ###转变为-1~1
         ])
    json_path = './class_indices.json'
    assert os.path.exists(json_path), "file:'{}'does not exist. ".format(json_path)
    with open(json_path, 'r') as f:
        class_indict = json.load(f)
    # create model
    model = vgg(model_name="vgg13", num_classes=5).to(device)

    weights_path = "./vgg13.pth"
    assert os.path.exists(weights_path), "file:'{}' does not exist.".format(weights_path)

    model.load_state_dict(torch.load(weights_path, map_location=device))
    vgg_hook = VGGHook(model)

    model.eval()

    path={
        'daisy':"./flow_feature/daisy.jpg",
        'dande':'./flow_feature/dande.jpg',
        'rose':'./flow_feature/rose.jpg',
        'sunflow':'./flow_feature/sunflow.jpg',
        'tulips':'./flow_feature/tulips.jpg'
    }
    resize = transforms.Resize(size=(150, 150), antialias=None)
    with torch.no_grad():
        for name,path in path.items():
            print(name,"="*100)
            img=Image.open(path)
            plt.imshow(img)##Image和imshow()是一脉相承的
            img=img.convert("RGB")
            ###############两种方法变成4维,模拟(N*C*H*W)##########
            # img=data_transform(img)[None]##将(3,244,244)---->(1,3,244,244),值变为[-1,1]
            img = data_transform(img)
            img = torch.unsqueeze(img, dim=0)  # img:(1,3,244,244)
            ###############两种方法变成4维,模拟(N*C*H*W)##########


            # ########给hook传特征处理后的图片######
            r=model(img.to(device))
            predict_label_idx = torch.argmax(r, dim=1)  # 选择每行数据中值最大的那个对应的下标,[1]
            print(r.shape)
            print(predict_label_idx)
            print(r[0][predict_label_idx.item()])
            ########给hook传图片###
            output = torch.squeeze(model(img.to(device))).cpu()  # tensor([a,s,d,f,g])
            print(output.size())  # torch.Size([5]) 5*1
            predict = torch.softmax(output, dim=0)
            """Softmax是一种数学函数,通常用于将一组任意实数转换为表示概率分布的实数。其本质上是一种归一化函数,可以将一组任意的实数值转化为在[0, 1]之间的概率值,
            因为softmax将它们转换为0到1之间的值,所以它们可以被解释为概率。如果其中一个输入很小或为负,softmax将其变为小概率,如果输入很大,
            则将其变为大概率,但它将始终保持在0到1之间。

            二维情况下,dim=0时,对列进行计算
            二维情况下,dim=1时,对行进行计算

            三维情况下:
            当 dim=0时, 是对每一维度相同位置的数值进行 softmax运算,和为1
            当 dim=1时, 是对某一维度的列进行 softmax运算,和为1
            当 dim=2时, 是对某一维度的行进行 softmax运算,和为1

            """
            predict_cla = torch.argmax(predict).numpy()

            print_res = "class:{} prob:{:.3}".format(class_indict[str(predict_cla)],
                                                 predict[predict_cla].numpy())
            plt.title(print_res)

        # for i in range(len(predict)):
        #     print("class:{:10} prob:{:.3}".format(class_indict[str(i)],
        #                                           predict[i].numpy()))
            plt.show()

            # 输出保存
            output_dir = os.path.join(f"./output/vgg", name)
            # if os.path.exists(output_dir):
            #     shutil.rmtree(output_dir)  # 删除文件夹
            if not os.path.exists( output_dir):
                os.makedirs(output_dir)  # 创建文件夹

            for idx, img in vgg_hook.imgs.items():
                # img的形式是[N,C,H,W] --> 此时N恒定为1,C表示C个通道,我们希望每个通道就是一张图像
                # 目标:将所有通道数据保存下来的
                # 需要对维度进行转换, [N,C,H,W] --> [C,N,H,W]

                n, _, _, _ = img.shape#图片的个数
                for i in range(n):
                    img_ = img[i:i + 1]  # [1,C,H,W]

                    img_ = img_.permute(1, 0, 2, 3)  # [N,C,H,W] --> [C,N,H,W]
                    img_ = resize(img_)
                    torchvision.utils.save_image(
                        img_,
                        os.path.join(output_dir, f"img_{i}_layer_{idx}.png"),#150*8=1200
                        nrow=8  # 每行有8个图像
                    )

        ddd=0

;