File size: 7,320 Bytes
936b2de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae5b77a
936b2de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
---
language:
- en
license: apache-2.0
tags:
- medical
- radiology
- image-captioning
- resnet-50
- transformers
- pytorch
- vision-and-language
datasets:
- eltorio/ROCOv2-radiology
---

# Model Card: ResNet-50 Graph Radiology Captioner

## Model Details
- **Model Name:** ResNet-50 Graph Radiology Captioner
- **Architecture:** Custom Encoder-Decoder with Graph Propagation
- **Language:** English
- **Dataset:** eltorio/ROCOv2-radiology
- **License:** Apache-2.0

## Model Description
This model is designed to generate automated textual descriptions (captions) for radiology images. It utilizes a hybrid architecture combining Convolutional Neural Networks (CNNs) with Graph Propagation and Transformer decoders.

The architecture consists of three main components:
1.  **Visual Encoder:** A pre-trained `ResNet-50` (from Hugging Face) extracts visual features from the input image.
2.  **Graph Propagation Layer:** A custom layer processes the spatial feature map using a sliding window mechanism (window size $3$) to aggregate neighborhood information over $3$ steps. This allows the model to better capture spatial relationships between anatomical structures.
3.  **Textual Decoder:** A standard Transformer Decoder (4 layers, 8 heads) generates the caption token by token, attending to the enhanced visual features.

## Intended Uses
- **Primary Use:** Generating preliminary descriptive reports for medical imaging datasets.
- **Research:** Investigating graph-based spatial reasoning in medical image captioning.

### Limitations
- The model may generate repetitive phrases or hallucinate findings not present in the image.
- The inference logic relies on a greedy decoder; beam search could improve results.
- Performance is strictly tied to the ROCOv2 dataset distribution; generalizability to other clinical domains may vary.

## Training Data
The model was trained on the **ROCOv2-radiology** dataset.

- **Dataset Split:**
  - Train: 56,963 images
  - Validation: 2,999 images
- **Image Modality:** Radiology (X-ray, CT, MRI, etc.)
- **Preprocessing:** Images resized to $224 \times 224$, normalized using ImageNet statistics, and augmented with random horizontal flips and rotations ($\pm 10^\circ$).

## Training Procedure
The model was trained for 15 epochs on a TPU environment using the CrossEntropyLoss function.

### Hyperparameters
| Parameter | Value |
| :--- | :--- |
| **Batch Size** | 200 |
| **Learning Rate** | $1 \times 10^{-4}$ |
| **Optimizer** | AdamW |
| **Epochs** | 15 |
| **Max Sequence Length** | 80 |
| **Embedding Dimension** | 512 |
| **Feed Forward Dimension** | 1024 |
| **Decoder Layers** | 4 |
| **Attention Heads** | 8 |
| **Graph Window Size** | 3 |
| **Graph Propagation Steps** | 3 |

### Training Results
The model achieved a steady decrease in training loss and perplexity over 15 epochs.

- **Final Training Loss:** $2.4942$
- **Final Perplexity:** $12.1121$

| Epoch | Training Loss | Perplexity |
| :--- | :--- | :--- |
| 1 | 6.0415 | 420.53 |
| 5 | 3.3029 | 27.19 |
| 10 | 2.7978 | 16.41 |
| 15 | 2.4942 | 12.11 |

## Evaluation
### Inference Sample
Below is a comparison between the ground truth caption and the model's generation on a validation set image.

**Ground Truth:** "Figure 1: PICC outlined by the arrows."
**Generated Caption:** "radiograph of the abdomen showing the presence of a large, well - defined, oval, calcified, and calcifications in the distal part of the right lower quadrant ( arrows )."

## How to Use
To use this model, you need to install `torch`, `transformers`, and `pillow`. The model architecture must be defined in your environment to load the weights.
```python
import torch
import torch.nn as nn
from transformers import BertTokenizer, AutoModel
from PIL import Image
from torchvision import transforms

# 1. Define the Model Architecture (Must match training script)
class GraphPropagationLayer(nn.Module):
def __init__(self, embed_dim, window_size=3, max_steps=3):
super().__init__()
self.window_size = window_size
self.max_steps = max_steps
self.msg_linear = nn.Linear(embed_dim, embed_dim)
self.activation = nn.Hardtanh()

def forward(self, x):
B, L, D = x.shape
pad = self.window_size // 2
for _ in range(self.max_steps):
neighbors = []
for i in range(-pad, pad + 1):
neighbors.append(torch.roll(x, shifts=i, dims=1))
stacked = torch.stack(neighbors, dim=2)
aggregated = stacked.mean(dim=2)
messages = self.msg_linear(aggregated)
x = self.activation(x + messages)
return x

class ResNetGraphCaptioner(nn.Module):
def __init__(self, vocab_size, embed_dim, num_layers, num_heads):
super().__init__()
self.encoder = AutoModel.from_pretrained("microsoft/resnet-50", trust_remote_code=True)
self.encoder.config.pooling = None
encoder_dim = self.encoder.config.hidden_sizes[-1]

self.vis_proj = nn.Linear(encoder_dim, embed_dim)
self.graph_layer = GraphPropagationLayer(embed_dim, window_size=3, max_steps=3)

self.embedding = nn.Embedding(vocab_size, embed_dim)
self.pos_encoder = nn.Parameter(torch.randn(1, 2000, embed_dim))

decoder_layer = nn.TransformerDecoderLayer(d_model=embed_dim, nhead=num_heads, dim_feedforward=1024, batch_first=True)
self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers)
self.fc_out = nn.Linear(embed_dim, vocab_size)

def forward(self, images, captions):
# Encode
outputs = self.encoder(pixel_values=images)
B, C, H, W = outputs.last_hidden_state.shape
visual_features = outputs.last_hidden_state.permute(0, 2, 3, 1).reshape(B, H*W, C)
visual_tokens = self.vis_proj(visual_features)
memory = self.graph_layer(visual_tokens)

# Decode
caption_emb = self.embedding(captions)
seq_len = caption_emb.size(1)
caption_emb = caption_emb + self.pos_encoder[:, :seq_len, :]

tgt_mask = torch.triu(torch.ones((seq_len, seq_len)) * float('-inf'), diagonal=1).to(images.device)
output = self.decoder(tgt=caption_emb, memory=memory, tgt_mask=tgt_mask)
return self.fc_out(output)

# 2. Load Checkpoint
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# Initialize model with specific config used during training
model = ResNetGraphCaptioner(
vocab_size=tokenizer.vocab_size, 
embed_dim=512, 
num_layers=4, 
num_heads=8
)

# Load weights (assuming you have the checkpoint file locally)
checkpoint_path = "erfanasghariyan/resnet50_roco_radiocaptioner_big/roco_best_model.bin"
model.load_state_dict(torch.load(checkpoint_path, map_location=device))
model.to(device)
model.eval()

# 3. Inference Function
def generate_caption(image_path, model, tokenizer, max_len=80):
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

image = Image.open(image_path).convert('RGB')
img_tensor = transform(image).unsqueeze(0).to(device)

input_ids = [tokenizer.cls_token_id]

with torch.no_grad():
for _ in range(max_len):
tensor_input = torch.tensor([input_ids], device=device)
outputs = model(img_tensor, tensor_input)
next_token = outputs[0, -1, :].argmax().item()

if next_token == tokenizer.sep_token_id:
break
input_ids.append(next_token)

return tokenizer.decode(input_ids[1:], skip_special_tokens=True)

# Example Usage
# caption = generate_caption("path/to/xray.jpg", model, tokenizer)
# print(caption)