دسته‌بندی نشده

توضیح نویسی برای تصاویر با هوش مصنوعی

تولید-توضیحات-متنی-برای-تصویر

در این مطلب قصد داریم یکی از پروژه های جذاب هوش مصنوعی را انجام دهیم. در پروژه عنوان بندی تصاویر ما یک تصویر را به عنوان ورودی میدهیم و به به عنوان خروجی، یک جمله که شامل یک توصیف متنی از تصویر است را دریافت میکنیم؛

در مطالب قبلی با روند پردازش تصویر و متن ها آشنا شدیم، برای پردازش یک تصویر از شبکه های کانولوشنال (CNN) استفاده میکردیم و برای متون نیز از شبکه های عصبی بازگشتی (RNN) کمک میگرفتیم.

معماری شبکه ای که امروز با آن آشنا می شویم ترکیبی از شبکه های CNN و RNN است. ابتدا تصاویر توسط یک شبکه کانولوشنی پردازش می شوند و یک بردار ویژگی برای هر عکس بدست می‌آوریم. این بردار ویژگی به عنوان ورودی به یک شبکه برگشتی مانند LSTM داده میشود تا با مدل سازی زبان بتواند یک جمله در وصف عکس متناظر تولیدکند؛

معماری رمزگزار-رمزگشا (encoder-decoder)

معماری رمزگزار-رمزگشا یا encoder-decoder که در پاراگراف بالا توضیح دادیم و در تصویر زیر نیز قابل مشاهده است، امکان اجرا و آموزش یکجای مدل های ترکیبی را فراهم میکند.

معماری-رمزگزار-رمزگشا

معماری-رمزگزار-رمزگشا

کدنویسی پروژه با پایتون

در ادامه مراحل اجرایی یک پروژه Image Captioning را ملاحظه میکنید:

کدها و تصاویر استفاده شده در مطلب، از گیت‌هاب استادرضوی برداشته شده است. برای دریافت سورس کامل به این (لینک) مراجعه کنید؛

وارد کردن کابخانه های مورد نیاز:
pack_padded_sequence کار Padding برای یکسان سازی سایز جملات را انجام میدهد

%reload_ext autoreload
%autoreload 2
%matplotlib inline

import os
import sys
import json
import pickle
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from glob import glob
from IPython.display import display

import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn.utils.rnn import pack_padded_sequence

import torchvision.transforms as transforms
import torchvision.models as models

from utils import *
from build_vocab import build_vocab
from data_loader import get_loader

# setup
use_gpu = torch.cuda.is_available()

توابع کمکی:

def load_cnn_model(model_name, pretrained=True):
    "Load and return a convolutional neural network."
    assert model_name in ['resnet18', 'resnet34', 'resnet50', 'resnet101', 'resnet152']
    return models.__dict__[model_name](pretrained)


def load_image(image_path, transform=None):
    "Load an image and perform given transformations."
    image = Image.open(image_path)    
    if transform is not None:
        image = transform(image).unsqueeze(0)
    return image

داده ها
برای این پروژه از دیتاست COCO استفاده میکنیم (لینک دیتاست)
البته چون قصد Persian Image Captioning داریم، برخی از داده ها ترجمه شده اند

دیتاست کوکو برای ایمیج کپشنینگ

coco-dataset-caption

dataset = load_json('data/fa_images_captions_train.json')

ساخت واژگان

DATA_DIR = 'data'
captions_filename = f'{DATA_DIR}/fa_captions.txt'
vocab_filename = f'{DATA_DIR}/vocab.pkl'

if os.path.exists(vocab_filename):
    vocab = pickle.load(open(vocab_filename, 'rb'))
else:
    vocab = build_vocab(captions_filename, min_count=3)
    pickle.dump(vocab, open(vocab_filename, 'wb'))

با اجرای کد کلمات بیشتر تکرار شده در دیتاست را مشاهده میکنید

for i in range(20):
    print("%s --> %d" %(vocab.idx2word[i], i))

Data loader

images_dir = f'{DATA_DIR}/images'
captions_json = f'{DATA_DIR}/fa_images_captions_train.json'
image_size = 256
crop_size  = 224
batch_size = 16


transform = transforms.Compose([
    transforms.Resize(image_size),
    transforms.RandomCrop(crop_size),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406),
                         (0.229, 0.224, 0.225))
])
data_loader = get_loader(images_dir, captions_json, vocab, 
                         transform, batch_size, 
                         shuffle=True, num_workers=0)
imgs, caps, lengths = next(iter(data_loader))

print(" ".join([str(id) for id in caps[0][1:-1]]))
print(" ".join([vocab.idx2word[id] for id in caps[0][1:-1]]))

ساخت مدل
همانطور که توضیح دادیم، برای این پروژه به معماری رمزگزار-رمزگشا نیاز داریم و مدل ما از دو بخش پیوسته به هم تشکیل میشود.
ابتدا برای پردازش تصویر یک شبکه کانولوشنال ایجاد میکنیم
1. Encoder (CNN)

class EncoderCNN(nn.Module):
    def __init__(self, model_name, embed_size):
        super(EncoderCNN, self).__init__()
        
        # load cnn and remove last layer
        cnn = load_cnn_model(model_name)
        modules = list(cnn.children())[:-1]  # remove last layer
        
        self.cnn = nn.Sequential(*modules)
        self.linear = nn.Linear(cnn.fc.in_features, embed_size)
        self.bn = nn.BatchNorm1d(embed_size, momentum=0.01)
        self.init_weights()
                
    def init_weights(self):
        self.linear.weight.data.normal_(0, 0.02)
        self.linear.bias.data.fill_(0)
        
    def forward(self, x):
        x = self.cnn(x)  # extract features from input image
        x = Variable(x.data)
        x = x.view(x.size(0), -1)
        x = self.linear(x)
        x = self.bn(x)
        return x
    
    def fine_tune(self, requires_grad=True):
        for param in self.cnn.layer4.parameters():
            param.requires_grad = requires_grad

2. Decoder (LSTM)

class DecoderLSTM(nn.Module):
    def __init__(self, vocab_size, embed_size, hidden_size, num_layers, dropout, tie_weights):
        super(DecoderLSTM, self).__init__()
        
        if tie_weights:
            embed_size = hidden_size
            
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True, dropout=0.35)
        self.fc = nn.Linear(hidden_size, vocab_size)
        self.dropout = nn.Dropout(p=dropout)
        
        if tie_weights:
            # share weights between embedding and classification layer
            self.fc.weight = self.embedding.weight
            
        self.init_weights()
        
    def init_weights(self):
        self.embedding.weight.data.uniform_(-0.1, 0.1)
        self.fc.weight.data.uniform_(-0.1, 0.1)
        self.fc.bias.data.fill_(0)
        
    def forward(self, features, captions, lengths):
        x = self.embedding(captions)
        x = torch.cat([features.unsqueeze(1), x], dim=1)
        x = self.dropout(x)
        x = pack_padded_sequence(x, lengths, batch_first=True)
        x, _ = self.lstm(x)
        x = self.dropout(x[0])
        x = self.fc(x)
        return x
    
    def sample(self, features, states=None):
        """Samples captions for given image features (Greedy search)."""
        sampled_ids = []
        inputs = features.unsqueeze(1)

        for i in range(20):                                      # maximum sampling length
            hiddens, states = self.lstm(inputs, states)          # (batch_size, 1, hidden_size), 
            outputs = self.fc(hiddens.squeeze(1))                # (batch_size, vocab_size)
            token_id = outputs.max(1)[1]
            sampled_ids += [token_id]
            inputs = self.embedding(token_id)
            inputs = inputs.unsqueeze(1)                         # (batch_size, 1, embed_size)
        sampled_ids = torch.cat(sampled_ids, 0)                  # (batch_size, 20)
        return sampled_ids.squeeze()

معماری رمزگزار-رمزگشا (encoder-decoder)

class EncoderDecoder(nn.Module):
    
    def __init__(self, cnn_name, vocab_size, embed_size, hidden_size, num_layers, dropout, tie_weights):
        super(EncoderDecoder, self).__init__()
        
        if tie_weights:
            embed_size = hidden_size
            
        self.encoder = EncoderCNN(cnn_name, embed_size)
        self.decoder = DecoderLSTM(vocab_size, embed_size, hidden_size, num_layers, dropout, tie_weights)
        
        # create output folder to save weights
        self.save_path = f'{cnn_name}-{embed_size}-{hidden_size}-{num_layers}'
        if not os.path.exists(self.save_path):
            os.mkdir(self.save_path)
    
    def forward(self, images, captions, lengths):
        features = self.encoder(images)
        outputs = self.decoder(features, captions, lengths)
        return outputs
    
    def save(self, epoch, loss):
        torch.save({'encoder': self.encoder.state_dict(), 
                    'decoder': self.decoder.state_dict()}, f'{self.save_path}/{epoch}-{loss:.2f}.pth')
    
    def load(self, epoch):
        model_path = glob(f'{self.save_path}/{epoch}-*.pth')[-1]
        try:
            d = torch.load(model_path)
            self.encoder.load_state_dict(d['encoder'])
            self.decoder.load_state_dict(d['decoder'])
        except:
            print('Invalid epoch number <{}>, the model does not exist!'.format(epoch))

پارامترها

# model hyper-parameters
cnn_name = 'resnet50'
embed_size  = 512
hidden_size = 512
num_layers  = 2
tie_weights = True

# training hyper-parameters
start_epoch = 0
num_epochs  = 20
learning_rate = 0.001

Training آموزش مدل

def train_epoch(model, train_dl, criterion, optimizer, scheduler, epoch, last_epoch):
    model.encoder.train()
    model.decoder.train()
    scheduler.step()
    
    total_steps = len(train_dl)
    epoch_loss = 0.0
    
    for i, (images, captions, lengths) in enumerate(train_dl):
        images, captions = to_var(images), to_var(captions)
        targets = pack_padded_sequence(captions, lengths, batch_first=True)[0]
        
        # forward step
        outputs = model(images, captions, lengths)
        loss = criterion(outputs, targets)
        epoch_loss = (epoch_loss * i + loss.data[0]) / (i + 1)
        
        # backward step
        model.encoder.zero_grad()
        model.decoder.zero_grad()
        
        loss.backward()
        torch.nn.utils.clip_grad_norm(model.decoder.parameters(), 5.0)
        optimizer.step()
        
        # report log info
        sys.stdout.flush()
        sys.stdout.write('\rEpoch [%2d/%2d], Step [%3d/%3d], Loss = %.4f, Perplexity = %.4f    '
                         % (epoch+1, last_epoch, i+1, total_steps, epoch_loss, np.exp(epoch_loss)))
    print()

    return epoch_loss


def train(model, train_dl, criterion, optimizer, scheduler, start_epoch=0, num_epochs=10):
    last_epoch = start_epoch + num_epochs
        
    for epoch in range(start_epoch, last_epoch):                
        # train step
        trn_loss = train_epoch(model, data_loader, criterion, optimizer, scheduler, epoch, last_epoch)
        
        # save model
        model.save(epoch, trn_loss)
model = EncoderDecoder(cnn_name, len(vocab), embed_size, hidden_size, num_layers, 0.3, tie_weights)
if use_gpu:
    model = model.cuda()

تعریف تابع هزینه و بهینه سازی (Loss and optimizer)

# loss function
criterion = nn.CrossEntropyLoss()
if use_gpu:
    criterion = criterion.cuda()
    
# list of parameters which will be updated
params = list(model.decoder.parameters())
params += list(model.encoder.linear.parameters()) 
params += list(model.encoder.bn.parameters())

# optimizer
optimizer = torch.optim.RMSprop(params, lr=learning_rate)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.97)

و با اجرای کد زیر مدل شروع به آموزش دیدن میکنید که مقداری زمانبر است و به قدرت سیستم عامل استفاده شده بستگی دارد:

train(model, data_loader, criterion, optimizer, scheduler, start_epoch, num_epochs)

نمونه برداری: ساخت توضیح برای تصاویر
بعد از آموزش مدل، حال نوبت تست کردن و ساخت کپشن برای تصاویر انتخابی است

from PIL import Image

val_transform = transforms.Compose([
    transforms.Resize(image_size),
    transforms.CenterCrop(crop_size),
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406),
                         (0.229, 0.224, 0.225))
])
def generate_caption(model, img_filenames):
    model.encoder.eval()
    model.decoder.eval()
    
    captions = []
    
    for img_filename in img_filenames:

        # prepare test image
        image = load_image(img_filename, val_transform)
        image_tensor = to_var(image, volatile=True)

        # Generate features from image
        feature = model.encoder(image_tensor)

        # Generate caption from image
        sampled_ids = model.decoder.sample(feature)
        sampled_ids = sampled_ids.cpu().data.numpy()

        # decode word ids to words
        sampled_caption = []
        for word_id in sampled_ids:
            word = vocab.idx2word[word_id]
            if word == '<EOS>': break
            sampled_caption.append(word)

        caption = " ".join(sampled_caption[1:])
        captions.append((img_filename, caption))
    
    return captions
img_filenames = glob('data/images/*.jpg')[:10]
captions = generate_caption(model, img_filenames)

for img, caption in captions:
    display(show_persian_image_and_caption(caption, img))
img_filenames = glob('./data/im2txt/*.jpg')[:10]
captions = generate_caption(model, img_filenames)

for img, caption in captions:
    display(show_persian_image_and_caption(caption, img))

کد های بالا حدود ده تصویر را به همراه توضیح ساخته شده توسط مدل، به ما نمایش میدهند؛

 

روش های بهینه سازی مدل

  1. استفاده از دیتاست بزرگتر و داده های بیشتر
  2. استفاده از بردار واژگان از قبل آموزش دیده
  3. استفاده از شبکه برگشتی دوسویه BiLSTM
  4. افزایش عمق مدل
  5. و…

 

author-avatar

درباره محمد اسماعیلی

علاقه مند به مفاهیم هوش مصنوعی، دیتاساینس و سئو؛ مطالبی که برام جالب باشه رو اینجا می نویسم، و این دلیل بر متخصص بودن من در اون حوزه ها نمیشه😊

نوشته های مرتبط

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *