本文的重点是 Vision Transformer(ViT)及其在现实生活问题中的实际应用。讨论了我已经使用卷积神经网络(CNN)解决并在这里介绍过的医学图像分类任务。现在,我展示基于ViT的解决方案。Transformer架构已成为自然语言处理任务的事实标准。那么,什么是视觉变换器(ViT)呢?ViT架构是基于将图像表示为一组补丁。图像补丁是大小为16x16像素的不重叠的图像块。例如,在分辨率为224x224的图像中,有(224 / 16)*(224 / 16)= 14 * 14 = 196个补丁。图像补丁被视为跟自然语言处理(NLP)应用中的令牌(单词)一样。ViT将每个补丁表示为其像素的平坦化线性投影,并操作长度为768的补丁嵌入向量(16x16x3 = 768)。下面的图片展示了ViT的完整架构:
变压器的主要部分如下:补丁+位置嵌入的准备、编码器、池化(多层池化头)。
Hugging Face 的 ViT:实践中的实现理解
让我们使用以下代码块来查看Hugging Face的基础ViT模型:
安装:
!pip install torchvision
!pip install torchinfo
!pip install -q git+https://github.com/huggingface/transformers.git
Imports:
from PIL import Image
from torchinfo import summary
import torch
Google Drive挂载(适用于Google Colab):
from google.colab import drive
drive.mount('/content/gdrive')
Cuda设备设置:
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
在下面的代码中,我查看了ViT基础模型:
from transformers import ViTConfig, ViTModel
configuration = ViTConfig()
print(configuration)
默认的基础模型配置如下:
ViTConfig {
"attention_probs_dropout_prob": 0.0,
"encoder_stride": 16,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.0,
"hidden_size": 768,
"image_size": 224,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"model_type": "vit",
"num_attention_heads": 12,
"num_channels": 3,
"num_hidden_layers": 12,
"patch_size": 16,
"qkv_bias": true,
"transformers_version": "4.37.0.dev0"
}
通过更改配置的字段,我们可以创建自定义的ViT模型。让我们尝试默认的ViT基础模型:
model = ViTModel(configuration).to(device)
model.eval()
在输出中,我们看到了所有ViT基础模型层:
模型概览:
summary(model=model, input_size=(1, 3, 224, 224), col_names=['input_size', 'output_size', 'num_params', 'trainable'])
ViT基础模型有大量的参数 — 超过8600万个。
让我们看模型输出的结构。我向模型发送了随机生成的假图片:
x = torch.randn((3, 224, 224))
x = torch.unsqueeze(x, 0)
y = model(x.to(device))
print(y.pooler_output.shape)
print(y.last_hidden_state.shape)
我们可以看到输出中:
torch.Size([1, 768])
torch.Size([1, 197, 768])
ViT基础模型的最终输出包含两部分:last_hidden_state的形状是(batch_size, 197, 768),它是序列模型.embeddings + model.encoder + model.layernorm在model.pooler部分之前的输出;pooler_output的形状是(batch_size, 768),它是model.pooler的输出。在model.pooler模块的输入中,有一个归一化last_hidden_state矩阵的零位置行,这是在前一步获得的。下面的图片阐释了逐步调用块(如上所述)与一次性调用整个模型获取模型输出的等效性:
如果我们在上图中用相同的输入张量x运行左边和右边的代码,我们会在打印时看到相同的输出张量。
了解ViT块及其输出结构对于基于ViT使用迁移学习开发解决方案来说是重要的。Model.pooler块被更改为自定义块,这个块使用ViT模型对之前块的推理作为输入来进行训练。
在Hugging Face上提供了两个预训练的ViT图像分类模型:
微调在ImageNet上用于1000类别分类的架构(ViTForImageClassification)包含model.classifier块而不是model.pooler块,仅以下线性层:
(classifier): Linear(in_features=768, out_features=1000, bias=True)
这一层的输入是标准化的last_hidden_state矩阵的零位置行。
ViT与CNN对比
如果从头开始训练,ViT 在小型定制数据集上不会表现出良好的性能。小型定制数据集的一个使用案例是使用 ViT 推断预先在大型数据集上训练的模型进行迁移学习。
ViT 用于X射线胸部图像分类
我使用的是相同的带有 X 射线胸部图像的数据集。这个数据集包含三类图像:
我使用统一裁剪的图像,其中包括胸部区域。裁剪图像的示例(从左至右分别为“正常(无肺炎)”,“细菌性肺炎”,“病毒性肺炎”):
该数据集被分为训练集和测试集。训练集包含3000张图片 — 1000张“正常(无肺炎)”,1000张“肺炎-细菌”,1000张“肺炎-病毒”,都是从各自的组别中随机选取的。其余的图片构成了测试集,因此测试集含有2908张图片 — 576张“正常(无肺炎)”,1777张“肺炎-细菌”,555张“肺炎-病毒”。
CNN与ViT对于“正常(无肺炎)”/“肺炎(细菌或病毒)”的2类分类器的比较
我正在使用X射线数据解决以下任务:创建一个系统,该系统可以确定输入的X射线胸部图像属于类别“正常(无肺炎)”或“肺炎(细菌或病毒)”,即2类分类器,使用ViT。这个模型展现了在此数据集上所有CNN模型中最佳的结果。以下是3个卷积模型的总结:
该模型包含348,050个参数,比ViT模型的参数少得多。注意,对于CNN模型,我使用的是分辨率为256x256的图像。
在这里,我尝试使用在ImageNet-21k数据集上预训练过的ViT模型,并对其进行微调以适用于X光图像。
型号1。ViT 处理输入图像后的“小型”线性分类器。
首先,我尝试最简单的解决方案, 一个线性层,其输入是last_hidden_state矩阵中零位置行, 有768个数值的向量。这种最终拟合适用于在ImageNet数据集上的1000个类别的图像分类器。
加载预训练的ViT模型+图像处理器:
from transformers import ViTConfig, ViTModel
from transformers import AutoImageProcessor
image_processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224-in21k")
model = ViTModel.from_pretrained("google/vit-base-patch16-224-in21k")
以下代码展示了将初始状态为PIL Image的单个图像经过预处理后,转换成预训练ViT模型的768个值的类标记向量的过程。这个向量是我的线性分类器的输入。
img = <load PIL Image>
inputs = image_processor(img, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
img = outputs.last_hidden_state
img = img[:, 0, :]
一个经过处理的输入图像批次,其形状为(batch_size, 1, 768),将被发送到以下模型:
class ChestClassifier(nn.Module):
def __init__(self, num_classes):
super(ChestClassifier, self).__init__()
self.num_classes = num_classes
self.ln = nn.Linear(768, self.num_classes)
def forward(self, x):
x = nn.Flatten()(x)
x = self.ln(x)
return x
model1 = ChestClassifier(2).to(device)
这个模型的摘要:
summary(model=model1, input_size=(1, 1, 768), col_names=['input_size', 'output_size', 'num_params', 'trainable'])
小型分类器模型仅包含1,538个参数。
我使用Adam优化器,学习率为0.001。用于训练的有3000张图像,用于测试的有2908张图像。我使训练批次保持平衡(每个类别约有50%的图像)。下方的图片展示了CNN架构的结果对比(这些结果已经获得并在这里呈现),以及上文提到的model1。在下方的结果中,“Class 0”代表“正常(无肺炎)”,而“Class 1”代表“肺炎(细菌或病毒)”。对于这两个模型,我都选择了最佳检查点:
使用“小”线性分类器对ViT模型进行细调的结果显然不如CNN架构的结果。我认为这些结果的原因如下:医疗图像与ViT模型训练时所用的ImageNet数据截然不同,而且我的“小”线性分类器的可训练参数数量不足以使迁移学习的结果优于CNN模型的结果。
如何改进模型呢?首先,没有什么能阻止我在细调分类器时使用完整的预训练patch-positional状态——即ViT输出的完整last_hidden_state。其次,我可以尝试一个更复杂的分类器模型,它具有更多的可训练参数。
模型2。ViT 处理输入图像后的“大型”线性分类器。
与模型1相比,我改变了输入PIL图像的预处理,以获得预训练ViT模型的完整转置last_hidden_state矩阵。这个矩阵构成了我的分类器模型的输入。
img = <load PIL Image>
inputs = image_processor(img, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
img = outputs.last_hidden_state.permute(0, 2, 1)
img = img.squeeze()
一个批次处理后的输入图像,形状为 (batch_size, 768, 197) 被发送到以下模型:
class ChestClassifierL(nn.Module):
def __init__(self, num_classes):
super(ChestClassifierL, self).__init__()
self.num_classes = num_classes
self.ln1 = nn.Linear(197, 256)
self.relu = nn.ReLU(inplace=True)
self.ln2 = nn.Linear(768*256, self.num_classes)
def forward(self, x):
x = self.ln1(x)
x = self.relu(x)
x = nn.Flatten()(x)
x = self.ln2(x)
return x
model2 = ChestClassifierL(2).to(device)
这个模型的总结:
summary(model=model2, input_size=(1, 768, 197), col_names=['input_size', 'output_size', 'num_params', 'trainable'])
"Large"分类器模型包含443,906个参数。
我使用Adam优化器,学习率=0.001。下面的图片展示了CNN架构的结果比较(它们已被获得并在此呈现)以及上述的model2。在下面的结果中,“Class 0”表示“正常(无肺炎)”,而“Class 1”表示“肺炎(细菌或病毒)”。对于这两个模型,我都选择了最佳的检查点:
使用“大”分类器进行ViT微调显示出比CNN更好的性能!这个结果的原因不仅仅是可训练参数数量的增加,而且考虑了整个补丁位置信息。分割概念对于医疗图像很重要,因为它们可能包含对特定问题特别的异常区域。
下面我展示了使用ViT在另一种分类器上的积极趋势——不同种类肺炎的分类器:“肺炎-细菌”和“肺炎-病毒”。我在训练集中有1000张“肺炎-细菌”的图片加上1000张“肺炎-病毒”的图片,并使用1777张“肺炎-细菌”的图片加上555张“肺炎-病毒”的图片进行测试。因此,训练集包含2000张图片,测试集包含2332张图片。我比较了相同的CNN架构,它有3个卷积块,以及与上面的分类器“正常(无肺炎)”/“肺炎(细菌或病毒)”相同的ViT加model2组合。在下面的结果中,“Class 0”表示“肺炎细菌”,“Class 1”表示“肺炎病毒”。对于这两个模型,我都选择了最佳的检查点:
模型3。微调 ViT 以实现自定义输入分辨率。
在上面讨论的所有例子中,我比较了在256x256分辨率输入图像上训练的CNN模型结果与ViT微调结果,其中ViT预训练模型要求输入图像具有224x224分辨率。在这篇文章中,我找到了在更高分辨率上进行迁移学习的解决方案:预训练模型的输出大小应该根据更高分辨率的嵌入位置改变,之后应该发送给模型进行新分辨率的微调。一个224x224的图像有196个补丁,ViT的最后隐藏状态分辨率是197x768。一个256x256的图像有256个补丁,ViT的最后隐藏状态分辨率应该是257x768。所以,为了对输入分辨率256x256的ViT进行微调,我需要将最后隐藏状态矩阵的分辨率调整为257x768,然后继续用这个矩阵进行训练。
让我们在实践中尝试一下。PIL图像的输入预处理将是以下步骤:
img = <load PIL Image>
inputs = image_processor(img, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
img = outputs.last_hidden_state.permute(0, 2, 1)
# new patch-position embeddings resolution
img = transforms.Resize((768, 257))(img)
img = img.squeeze()
已处理的输入图片批次以形状 (batch_size, 768, 257) 被发送到以下模型:
class ChestClassifierL256(nn.Module):
def __init__(self, num_classes):
super(ChestClassifierL256, self).__init__()
self.num_classes = num_classes
self.ln1 = nn.Linear(257, 256)
self.relu = nn.ReLU(inplace=True)
self.ln2 = nn.Linear(768*256, self.num_classes)
def forward(self, x):
x = self.ln1(x)
x = self.relu(x)
x = nn.Flatten()(x)
x = self.ln2(x)
return x
model3 = ChestClassifierL256(2).to(device)
这个模型的总结是:
summary(model=model3, input_size=(1, 768, 257), col_names=['input_size', 'output_size', 'num_params', 'trainable'])
我尝试了用于二分类的模型3:“正常(无肺炎)”/“肺炎(细菌或病毒)”。下图展示了模型2输入分辨率为224x224的结果与模型3输入分辨率为256x256的结果对比。在结果中,“类别0”代表“正常(无肺炎)”,而“类别1”代表“肺炎(细菌或病毒)”。对于这两个模型,我都选择了最佳的检查点:
解析度变更的结果在与224x224分辨率相去甚远的情况下更加显著。
结论
对ViT(Vision Transformer)推理和精调模型的适当组合,即使在如医学图像这样特定的数据集上,也可能提高分类器的性能。