李宏毅课程作业五 CNN Explaination

2023-11-01


本文主要是对课程里的代码加上自己的注解,记录下点滴知识

一、代码作业

1.环境设置

代码如下(示例):

# 下載並解壓縮訓練資料
!gdown --id '19CzXudqN58R3D-1G8KeFWk8UDQwlb8is' --output food-11.zip
!unzip food-11.zip
# 下載 pretrained model,這裡是用助教的 model demo,寫作業時要換成自己的 model
!gdown --id '1CShZHsO8oAZwxQkMe7jRtEgSNb2w_OZu' --output checkpoint.pth
# 安裝lime套件
# 這份作業會用到的套件大部分 colab 都有安裝了,只有 lime 需要額外安裝
!pip install lime==0.1.1.37

2.引入库

代码如下(示例):

import os
#os模块提供了多数操作系统的功能接口函数。
#当os模块被导入后,它会自适应于不同的操作系统平台,根据不同的平台进行相应的操作,
#在python编程时,经常和文件、目录打交道,这时就离不了os模块
import sys
#Python的sys模块提供访问由解释器使用或维护的变量的接口,并提供了一些函数用来和解释器进行交互,操控Python的运行时环境。
import argparse
# argparse 是 Python 内置的一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,
# argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息。
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.optim import Adam
from torch.utils.data import Dataset
import torchvision.transforms as transforms
from skimage.segmentation import slic#图像分割
from lime import lime_image#图像解释器库
from pdb import set_trace#pdb自带调试器

3.参数分析

args = {
      'ckptpath': './checkpoint.pth',
      'dataset_dir': './food-11/'
}
args = argparse.Namespace(**args)

4.定义模型

class Classifier(nn.Module):
  def __init__(self):
    super(Classifier, self).__init__()

    def building_block(indim, outdim):
      return [
        nn.Conv2d(indim, outdim, 3, 1, 1),#(in_channel, out_channel, kernel_size, stride, padding)
        nn.BatchNorm2d(outdim),
        nn.ReLU(),
      ]
    def stack_blocks(indim, outdim, block_num):
      layers = building_block(indim, outdim)
      for i in range(block_num - 1):
        layers += building_block(outdim, outdim)
      layers.append(nn.MaxPool2d(2, 2, 0))
      #class torch.nn.MaxPool1d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
      return layers

    cnn_list = []
    cnn_list += stack_blocks(3, 128, 3)
    cnn_list += stack_blocks(128, 128, 3)
    cnn_list += stack_blocks(128, 256, 3)
    cnn_list += stack_blocks(256, 512, 1)
    cnn_list += stack_blocks(512, 512, 1)
    self.cnn = nn.Sequential( * cnn_list)#Sequential定义训练框架
##############
#   self.cnn = nn.Sequential{

#}
##############
    dnn_list = [
      nn.Linear(512 * 4 * 4, 1024),
      nn.ReLU(),
      nn.Dropout(p = 0.3),
      nn.Linear(1024, 11),
    ]
    self.fc = nn.Sequential( * dnn_list)
    #########
#   self.fc = nn.Sequential{
#    nn.Linear(512*4*4, 1024)
#    nn.ReLU(),
#    nn.Dropout(p=0.3),
#    nn.Linear(1024,11)
#}
#########
  def forward(self, x):
    out = self.cnn(x)
    out = out.reshape(out.size()[0], -1)
    return self.fc(out)
model = Classifier().cuda()
checkpoint = torch.load(args.ckptpath)
model.load_state_dict(checkpoint['model_state_dict'])
# 基本上出現 <All keys matched successfully> 就是有載入成功,但最好還是做一下 inference 確認 test accuracy 沒有錯。

5.定义创建数据集

# 助教 training 時定義的 dataset
# 因為 training 的時候助教有使用底下那些 transforms,所以 testing 時也要讓 test data 使用同樣的 transform
# dataset 這部分的 code 基本上不應該出現在你的作業裡,你應該使用自己當初 train HW3 時的 preprocessing
class FoodDataset(Dataset):
    def __init__(self, paths, labels, mode):
        # mode: 'train' or 'eval'
        
        self.paths = paths
        self.labels = labels
        #数据预处理
        trainTransform = transforms.Compose([
            transforms.Resize(size=(128, 128)),#把图片调整至合适大小
            transforms.RandomHorizontalFlip(),#翻转图片
            transforms.RandomRotation(15),#按角度旋转图像。
            #torchvision.transforms.RandomRotation(degrees, resample=False, expand=False, center=None)
            #degrees(sequence 或float或int) -要选择的度数范围。
            #resample 可选的重采样过滤器
            #expand(bool,optional) - 可选的扩展标志。
            #如果为true,则展开输出以使其足够大以容纳整个旋转图像。如果为false或省略,则使输出图像与输入图像的大小相同
            #center(2-tuple ,optional) - 可选的旋转中心。原点是左上角。默认值是图像的中心。
            transforms.ToTensor(),
        ])
        evalTransform = transforms.Compose([
            transforms.Resize(size=(128, 128)),
            transforms.ToTensor(),
        ])
        self.transform = trainTransform if mode == 'train' else evalTransform

    # 這個 FoodDataset 繼承了 pytorch 的 Dataset class
    # 而 __len__ 和 __getitem__ 是定義一個 pytorch dataset 時一定要 implement 的兩個 methods
    def __len__(self):
        return len(self.paths)

    def __getitem__(self, index):
        X = Image.open(self.paths[index])
        X = self.transform(X)
        Y = self.labels[index]
        return X, Y

    # 這個 method 並不是 pytorch dataset 必要,只是方便未來我們想要指定「取哪幾張圖片」出來當作一個 batch 來 visualize
    def getbatch(self, indices):
        images = []
        labels = []
        for index in indices:
          image, label = self.__getitem__(index)#返回的x,y值
          images.append(image)
          labels.append(label)
        return torch.stack(images), torch.tensor(labels)

# 給予 data 的路徑,回傳每一張圖片的「路徑」和「class」
def get_paths_labels(path):
    imgnames = os.listdir(path)
    imgnames.sort()
    imgpaths = []
    labels = []
    for name in imgnames:
        imgpaths.append(os.path.join(path, name))
        labels.append(int(name.split('_')[0]))
    return imgpaths, labels
train_paths, train_labels = get_paths_labels(os.path.join(args.dataset_dir, 'training'))

# 這邊在 initialize dataset 時只丟「路徑」和「class」,之後要從 dataset 取資料時
# dataset 的 __getitem__ method 才會動態的去 load 每個路徑對應的圖片
train_set = FoodDataset(train_paths, train_labels, mode='eval')

6.显著性图片

我們把一張圖片丟進 model,forward 後與 label 計算出 loss。 因此與 loss 相關的有:

image
model parameter
label
通常的情況下,我們想要改變 model parameter 來 fit image 和 label。因此 loss 在計算 backward 時我們只在乎 loss 對 model parameter 的偏微分值。但數學上 image 本身也是 continuous tensor,我們可以計算 loss 對 image 的偏微分值。這個偏微分值代表「在 model parameter 和 label 都固定下,稍微改變 image 的某個 pixel value 會對 loss 產生什麼變化」。人們習慣把這個變化的劇烈程度解讀成該 pixel 的重要性 (每個 pixel 都有自己的偏微分值)。因此把同一張圖中,loss 對每個 pixel 的偏微分值畫出來,就可以看出該圖中哪些位置是 model 在判斷時的重要依據。

實作上非常簡單,過去我們都是 forward 後算出 loss,然後進行 backward。而這個 backward,pytorch 預設是計算 loss 對 model parameter 的偏微分值,因此我們只需要用一行 code 額外告知 pytorch,image 也是要算偏微分的對象之一。

def normalize(image):#标准化,归一化
  return (image - image.min()) / (image.max() - image.min())
#特征缩放是最重要的数据转换之一.
#可以用线性函数归一化(Normalization,减去最小值,除以最大值与最小值的差值,sklearn中的MinMaxScaler)
#和标准化(Standardization,减去平均值,除以方差,sklearn中的StandardScaler)来实现.

def compute_saliency_maps(x, y, model):
  model.eval()
  x = x.cuda()

  # 最關鍵的一行 code
  # 因為我們要計算 loss 對 input image 的微分,原本 input x 只是一個 tensor,預設不需要 gradient
  # 這邊我們明確的告知 pytorch 這個 input x 需要gradient,這樣我們執行 backward 後 x.grad 才會有微分的值
  x.requires_grad_()
  
  y_pred = model(x)
  loss_func = torch.nn.CrossEntropyLoss()
  loss = loss_func(y_pred, y.cuda())
  loss.backward()

  saliencies = x.grad.abs().detach().cpu()
  # saliencies: (batches, channels, height, weight)
  # 因為接下來我們要對每張圖片畫 saliency map,每張圖片的 gradient scale 很可能有巨大落差
  # 可能第一張圖片的 gradient 在 100 ~ 1000,但第二張圖片的 gradient 在 0.001 ~ 0.0001
  # 如果我們用同樣的色階去畫每一張 saliency 的話,第一張可能就全部都很亮,第二張就全部都很暗,
  # 如此就看不到有意義的結果,我們想看的是「單一張 saliency 內部的大小關係」,
  # 所以這邊我們要對每張 saliency 各自做 normalize。手法有很多種,這邊只採用最簡單的
  saliencies = torch.stack([normalize(item) for item in saliencies])
  return saliencies
# 指定想要一起 visualize 的圖片 indices
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
saliencies = compute_saliency_maps(images, labels, model)

# 使用 matplotlib 畫出來
fig, axs = plt.subplots(2, len(img_indices), figsize=(15, 8))
for row, target in enumerate([images, saliencies]):
  for column, img in enumerate(target):
    axs[row][column].imshow(img.permute(1, 2, 0).numpy())
    # 小知識:permute 是什麼,為什麼這邊要用?
    # 在 pytorch 的世界,image tensor 各 dimension 的意義通常為 (channels, height, width)
    # 但在 matplolib 的世界,想要把一個 tensor 畫出來,形狀必須為 (height, width, channels)
    # 因此 permute 是一個 pytorch 很方便的工具來做 dimension 間的轉換
    # 這邊 img.permute(1, 2, 0),代表轉換後的 tensor,其
    # - 第 0 個 dimension 為原本 img 的第 1 個 dimension,也就是 height
    # - 第 1 個 dimension 為原本 img 的第 2 個 dimension,也就是 width
    # - 第 2 個 dimension 為原本 img 的第 0 個 dimension,也就是 channels

plt.show()
plt.close()
# 從第二張圖片的 saliency,我們可以發現 model 有認出蛋黃的位置
# 從第三、四張圖片的 saliency,雖然不知道 model 細部用食物的哪個位置判斷,但可以發現 model 找出了食物的大致輪廓

7.解释性Filter explaination

這裡我們想要知道某一個 filter 到底認出了什麼。我們會做以下兩件事情:

Filter activation: 挑幾張圖片出來,看看圖片中哪些位置會 activate 該 filter
Filter visualization: 怎樣的 image 可以最大程度的 activate 該 filter
實作上比較困難的地方是,通常我們是直接把 image 丟進 model,一路 forward 到底。如:

loss = model(image)
loss.backward()

我們要怎麼得到中間某層 CNN 的 output? 當然我們可以直接修改 model definition,讓 forward 不只 return loss,也 return activation map。但這樣的寫法麻煩了,更改了 forward 的 output 可能會讓其他部分的 code 要跟著改動。因此 pytorch 提供了方便的 solution: hook,以下我們會再介紹。

model
def normalize(image):
  return (image - image.min()) / (image.max() - image.min())

layer_activations = None
def filter_explaination(x, model, cnnid, filterid, iteration=100, lr=1):
  # x: 要用來觀察哪些位置可以 activate 被指定 filter 的圖片們
  # cnnid, filterid: 想要指定第幾層 cnn 中第幾個 filter
  model.eval()

  def hook(model, input, output):
    global layer_activations
    layer_activations = output
  
  hook_handle = model.cnn[cnnid].register_forward_hook(hook)
  # 這一行是在告訴 pytorch,當 forward 「過了」第 cnnid 層 cnn 後,要先呼叫 hook 這個我們定義的 function 後才可以繼續 forward 下一層 cnn
  # 因此上面的 hook function 中,我們就會把該層的 output,也就是 activation map 記錄下來,這樣 forward 完整個 model 後我們就不只有 loss
  # 也有某層 cnn 的 activation map
  # 注意:到這行為止,都還沒有發生任何 forward。我們只是先告訴 pytorch 等下真的要 forward 時該多做什麼事
  # 注意:hook_handle 可以先跳過不用懂,等下看到後面就有說明了

  # Filter activation: 我們先觀察 x 經過被指定 filter 的 activation map
  model(x.cuda())
  # 這行才是正式執行 forward,因為我們只在意 activation map,所以這邊不需要把 loss 存起來
  filter_activations = layer_activations[:, filterid, :, :].detach().cpu()
  
  # 根據 function argument 指定的 filterid 把特定 filter 的 activation map 取出來
  # 因為目前這個 activation map 我們只是要把他畫出來,所以可以直接 detach from graph 並存成 cpu tensor
  
  # Filter visualization: 接著我們要找出可以最大程度 activate 該 filter 的圖片
  x = x.cuda()
  # 從一張 random noise 的圖片開始找 (也可以從一張 dataset image 開始找)
  x.requires_grad_()
  # 我們要對 input image 算偏微分
  optimizer = Adam([x], lr=lr)
  # 利用偏微分和 optimizer,逐步修改 input image 來讓 filter activation 越來越大
  for iter in range(iteration):
    optimizer.zero_grad()
    model(x)
    
    objective = -layer_activations[:, filterid, :, :].sum()
    # 與上一個作業不同的是,我們並不想知道 image 的微量變化會怎樣影響 final loss
    # 我們想知道的是,image 的微量變化會怎樣影響 activation 的程度
    # 因此 objective 是 filter activation 的加總,然後加負號代表我們想要做 maximization
    
    objective.backward()
    # 計算 filter activation 對 input image 的偏微分
    optimizer.step()
    # 修改 input image 來最大化 filter activation
  filter_visualization = x.detach().cpu().squeeze()[0]#指定维若维度为1,则删去
  # 完成圖片修改,只剩下要畫出來,因此可以直接 detach 並轉成 cpu tensor

  hook_handle.remove()
  # 很重要:一旦對 model register hook,該 hook 就一直存在。如果之後繼續 register 更多 hook
  # 那 model 一次 forward 要做的事情就越來越多,甚至其行為模式會超出你預期 (因為你忘記哪邊有用不到的 hook 了)
  # 因此事情做完了之後,就把這個 hook 拿掉,下次想要再做事時再 register 就好了。

  return filter_activations, filter_visualization
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
filter_activations, filter_visualization = filter_explaination(images, model, cnnid=15, filterid=0, iteration=100, lr=0.1)

# 畫出 filter visualization
plt.imshow(normalize(filter_visualization.permute(1, 2, 0)))
plt.show()
plt.close()
# 根據圖片中的線條,可以猜測第 15 層 cnn 其第 0 個 filter 可能在認一些線條、甚至是 object boundary
# 因此給 filter 看一堆對比強烈的線條,他會覺得有好多 boundary 可以 activate

# 畫出 filter activations
fig, axs = plt.subplots(2, len(img_indices), figsize=(15, 8))
for i, img in enumerate(images):
  axs[0][i].imshow(img.permute(1, 2, 0))
for i, img in enumerate(filter_activations):
  axs[1][i].imshow(normalize(img))
plt.show()
plt.close()
# 從下面四張圖可以看到,activate 的區域對應到一些物品的邊界,尤其是顏色對比較深的邊界

8.Lime

Lime 的部分因為有現成的套件可以使用,因此下方直接 demo 如何使用該套件。其實非常的簡單,只需要 implement 兩個 function 即可。

def predict(input):
    # input: numpy array, (batches, height, width, channels)                                                                                                                                                     
    
    model.eval()                                                                                                                                                             
    input = torch.FloatTensor(input).permute(0, 3, 1, 2)                                                                                                            
    # 需要先將 input 轉成 pytorch tensor,且符合 pytorch 習慣的 dimension 定義
    # 也就是 (batches, channels, height, width)

    output = model(input.cuda())                                                                                                                                             
    return output.detach().cpu().numpy()                                                                                                                              
                                                                                                                                                                             
def segmentation(input):
    # 利用 skimage 提供的 segmentation 將圖片分成 100 塊                                                                                                                                      
    return slic(input, n_segments=100, compactness=1, sigma=1)                                                                                                              
                                                                                                                                                                             
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
fig, axs = plt.subplots(1, 4, figsize=(15, 8))                                                                                                                                                                 
np.random.seed(16)                                                                                                                                                       
# 讓實驗 reproducible
for idx, (image, label) in enumerate(zip(images.permute(0, 2, 3, 1).numpy(), labels)):                                                                                                                                             
    x = image.astype(np.double)
    # lime 這個套件要吃 numpy array

    explainer = lime_image.LimeImageExplainer()                                                                                                                              
    explaination = explainer.explain_instance(image=x, classifier_fn=predict, segmentation_fn=segmentation)
    # 基本上只要提供給 lime explainer 兩個關鍵的 function,事情就結束了
    # classifier_fn 定義圖片如何經過 model 得到 prediction
    # segmentation_fn 定義如何把圖片做 segmentation
    # doc: https://lime-ml.readthedocs.io/en/latest/lime.html?highlight=explain_instance#lime.lime_image.LimeImageExplainer.explain_instance

    lime_img, mask = explaination.get_image_and_mask(                                                                                                                         
                                label=label.item(),                                                                                                                           
                                positive_only=False,                                                                                                                         
                                hide_rest=False,                                                                                                                             
                                num_features=11,                                                                                                                              
                                min_weight=0.05                                                                                                                              
                            )
    # 把 explainer 解釋的結果轉成圖片
    # doc: https://lime-ml.readthedocs.io/en/latest/lime.html?highlight=get_image_and_mask#lime.lime_image.ImageExplanation.get_image_and_mask
    
    axs[idx].imshow(lime_img)

plt.show()
plt.close()
# 從以下前三章圖可以看到,model 有認出食物的位置,並以該位置為主要的判斷依據
# 唯一例外是第四張圖,看起來 model 似乎比較喜歡直接去認「碗」的形狀,來判斷該圖中屬於 soup 這個 class
# 至於碗中的內容物被標成紅色,代表「單看碗中」的東西反而有礙辨認。
# 當 model 只看碗中黃色的一坨圓形,而沒看到「碗」時,可能就會覺得是其他黃色圓形的食物。

总结

目前还是初学阶段,望各位大佬发现错误时能够对我批评指正,多谢!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

李宏毅课程作业五 CNN Explaination 的相关文章

随机推荐

  • 人工智能革命:从AGI到ASI的道路

    在某种程度上 我们将获得具有人类一般智能的AGI计算机后 只是一群人和计算机平等地生活在一起 哦 实际上并不会 问题是 AGI具有与人类相同的智能和计算能力水平 但仍然具有优于人类的优势 如 硬件 速度 大脑的神经元最大频率大约为200赫兹
  • 数据分析--Python将dataframe格式数据存入clickhouse

    Python将dataframe格式数据存入clickhouse 基于Python实现大批量dataframe格式数据存入clickhouse中 python 一切 基于Python实现大批量dataframe格式数据存入clickhous
  • 信用卡风控——梯度提升树方法Python实现

    本文是一个用户按时还款的预测模型 利用台湾地区一些信用卡客户的信用额度 教育程度 婚姻状况 过去的还款状态 账单等信息对客户进行评分 采用GBDT 梯度提升树 模型对数据进行分类 预测其是否会产生逾期偿还 数据来源于kaggle的一个比赛
  • dispatch_async 与 dispatch_sync

    从字面意思上看 sync 同步 async 异步 并行队列 异步 异步 dispatch async dispatch get global queue DISPATCH QUEUE PRIORITY DEFAULT 0 dispatch
  • strcpy和strdup比较和详解

    strcpy和strdup比较和详解 函数和功能描述 extern char strdup char s 头文件 string h 功能 将串拷贝到新建的位置处 说 明 strdup不是标准的c函数 strdup 在内部调用了malloc
  • matlab中如何表示开方,在matlab里面怎么输入开方号(根号)?9的开方怎么写?...

    答 sqrt 9 nthroot 8 3 人家想求的就是实数根把 不信你试试 8 1 3 和 nthroot 8 3 你就知道区别了 很多人不知道nthroot这个函数 用指数的方法画x的3次方根的图像结果发现x 答 sqrt 9 nthr
  • 说说代码评审

    关注公众号 1024个为什么 及时接收最新推送文章 本文内容 1 代码评审的目的 2 评审准备工作 3 评审过程中容易出问题的点 4 共同成长 近一段时间以来 组内严格实行代码评审制度 参与过多次评审后发现 一次有效的代码评审并不简单 把一
  • ucint核心边缘分析_物联网加速器:边缘计算,万亿芯片新空间

    温馨提示 如需原文档 可在PC端登陆未来智库官网 www vzkoo com 搜索下载本文档 报告概览 什么是边缘计算 边缘计算被定义为 一种新的计算方式 这种模式将计算与存储资源部署在更贴近移动设备或传感器的网络边缘 其核心在于 贴近 终
  • 解决github无法访问的办法

    方法 步骤 1 问题描述 能联网但不能访问github com 2 找到hosts文件 地址 C Windows System32 drivers etc 一般是在这的 3 不要直接在这修改hosts文件 需要将hosts文件复制粘贴到桌面
  • 杨辉三角形(超级简单的Python实现方法)

    杨辉三角形大家都知道 那么在这里就不再介绍了 大家肯定都用C C 实现过杨辉三角形 是把数据存放在了一个二维数组里 那么如何让用Python来实现杨辉三角形呢 我查看了网上的很多方法 都很麻烦 有的还用到了生成器 那我们来看看简单的方法吧
  • 一个完整的项目管理流程

    一个完整的项目管理流程 从一个项目提出到结束 按照ISO9001 2000的项目管理流程 大致有如下步骤 1 产品立项报告 按照公司的管理流程 由公司有关人等都有可能提出 产品立项报告 比如公司老总 市场部门 研发部门 一般是在公司组织的定
  • 字符数组的学习总结

    今天主要讲了两点 一是字符数组的定义 二是字符数组的拷贝 也是重点 一 字符数组的定义 char arr 5 a b c d e char brr 5 a b c 聚合类型 只初始化部分 剩余部分为0 0 char crr a b c d
  • CTF逆向工程入门_1

    这篇文章写给众多参加东华网络安全大赛的小伙伴们 初次撰写 大佬们发现错误的地方可以指正出来哈 CTF逆向工程入门 1 http blog csdn net s1054436218 article details 71698904 CTF逆向
  • 刘强东的自白:我为啥要和苏宁打价格战?

    价格屠夫 挑事者 城府很深 这是很多人对于刘强东的评价 刘强东则辩白称 他没有想象的那样复杂 同时 他也自述了为何要与苏宁 国美展开价格战 以及此次价格战的京东兵法 精彩语录 1 李国庆他们就是擦皮鞋的 他们有能力打价格战吗 有能力的话不需
  • 【数据结构】顺序表(SeqList)(增、删、查、改)详解

    一 顺序表的概念和结构 1 顺序表的概念 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构 一般情况下采用数组存储 在数组上完成数据的增删查改 2 顺序表的结构 1 静态顺序表 使用定长数组存储元素 缺点 只适用于确定知道需要
  • AWS EC2使用过程总结

    步骤1 开通AWS账号 需要一个邮箱 一个信用卡账号 有第一年的免费试用 EC2每个月免费试用750小时 注册完成后 得到实例管理平台 步骤2 开通EC2实例 步骤3 开通网关和安全组 使外部可以访问 在任何一台电脑的浏览器输入云服务器的公
  • 3.微服务概述

    1 大型网络架构变迁 SOA与微服务最大的差别就是服务拆分的细度 目前大多数微服务实际上是SOA架构 真正的微服务应该是一个接口对应一个服务器 开发速度快 成本高 微服务 SOA 能拆分的就拆分 是整体的 服务能放一起的都放一起 业务逻辑存
  • java处理网络图片

    将网络图片地址转为InputStream 获取网络图片 HttpURLConnection connection HttpURLConnection new URL url openConnection connection setRead
  • webgoat全关教程手册

    Webgoat Webwolf owaspbwa里面的两个服务 搭建 先要安装jdk Webgoat和Webwolf Webgoat和Webwolf jdk1 8不支持了 需要安装jdk11 去git上下载Webgoat和Webwolf h
  • 李宏毅课程作业五 CNN Explaination

    李宏毅课程作业五 CNN Explaination 本文主要是对课程里的代码加上自己的注解 记录下点滴知识 一 代码作业 1 环境设置 2 引入库 3 参数分析 4 定义模型 5 定义创建数据集 6 显著性图片 7 解释性Filter ex