Руководство по выполнению комплексных проектов компьютерного зрения с PyTorch-Lightning, Comet ML и Gradio

Компьютерное зрение на данный момент является модным словечком. Однако создание успешных проектов в этой области сопряжено со многими проблемами. Это потому, что эти проекты требуют больших знаний математики, мощности компьютера и времени. К счастью, для решения этих проблем появилось множество современных фреймворков и библиотек.

Сегодня я расскажу вам, как реализовать сквозной проект классификации изображений с помощью библиотек Lightning, Comet ML и Gradio. Во-первых, мы создадим модель глубокого обучения с помощью Lightning. После этого мы будем отслеживать гиперпараметры, мониторить метрики и сохранять модель с помощью Comet ML. Наконец, мы создадим приложение для обнаружения рака с помощью Gradio. После завершения нашего проекта это приложение будет выглядеть так:

Вот темы, которые мы рассмотрим в этом блоге:

  • Что такое PyTorch-Lightning и Comet ML?
  • Подготовка данных с помощью модуля данных Lightning
  • Построение модели с помощью Lightning Module
  • Мониторинг метрик с Comet ML
  • Создание приложения для обнаружения рака с помощью Gradio

Давайте сначала взглянем с высоты птичьего полета на то, что такое Lightning и Comet ML.

PyTorch-молния

Как вы знаете, PyTorch — популярный фреймворк для построения моделей глубокого обучения. Однако, когда вы увеличиваете сложность модели, этапы моделирования, скорее всего, станут беспорядочными. Тут в дело вступает Молния.

Lightning — это высокоуровневая и гибкая платформа глубокого обучения, упрощающая управление кодом PyTorch. Используя Lightning, вы можете автоматизировать задачи обучения, такие как построение модели, загрузка данных, создание контрольных точек модели и ведение журнала. Чтобы начать работу с этим фреймворком, Lightning за 15 минут — отличный учебник.

Комета МЛ

В конце концов, наша цель — получить лучшую модель. Для этого мы стремимся найти оптимальное сочетание гиперпараметров. Вот тут-то и вступает в игру Комета МЛ.

Comet ML – это платформа MLOps, которая помогает отслеживать гиперпараметры, контролировать показатели модели и оптимизировать модель. Чтобы использовать платформу Comet ML, вы можете создать бесплатную учетную запись, посетив Comet. Если вы используете Comet ML впервые, вы можете ознакомиться с кратким руководством.

Давайте продолжим и воплотим то, о чем мы говорили, в действие с проектом.

Классификация изображений для обнаружения рака

Как мы все знаем, рак — это сложное и распространенное заболевание, от которого страдают миллионы людей во всем мире. К счастью, последние технологии компьютерного зрения позволили обнаружить рак по изображениям. В этом разделе мы расскажем, как это сделать.

Набор данных, который мы собираемся использовать, — это набор данных гистопатологического обнаружения рака.

Этот набор данных состоит из 327 680 цветных изображений, полученных путем извлечения их из гистопатологических сканов срезов лимфатических узлов. Если вы не знакомы с этой областью, не волнуйтесь. Наша цель — построить модель, которая сможет определить наличие канцерогенных элементов на изображении.

Пожалуйста, имейте в виду, что вы можете найти блокнот, который мы собираемся использовать в этом блоге, здесь. Начнем с изучения меток изображений.

# Reading the labels
cancer_labels = pd.read_csv("train_labels.csv")

# Visualizing the labels
labels_count = cancer_labels.label.value_counts()
plt.pie(labels_count, labels=['No Cancer', 'Cancer'], startangle=180, autopct='%1.1f')
plt.figure(figsize=(16,16))
plt.show()

Как видите, ярлыков «не рак» немного больше. Давайте перейдем к рассмотрению нескольких изображений в наборе данных.

# Visualizing the images
base_dir = 'histopathologic-cancer-detection/'
fig = plt.figure(figsize=(25, 6))
train_imgs = os.listdir(base_dir+"train")
for idx, img in enumerate(np.random.choice(train_imgs, 20)):
    ax = fig.add_subplot(2, 20//2, idx+1, xticks=[], yticks=[])
    im = Image.open(base_dir+"train/" + img)
    plt.imshow(im)
    lab = cancer_labels.loc[cancer_labels['id'] == img.split('.')[0], 'label'].values[0]
    ax.set_title('Label: %s'%lab)

Как видите, каждое изображение снабжено бинарной меткой, показывающей наличие метастатической ткани. Переходим к этапу предварительной обработки данных.

Предварительная обработка данных

Как мы уже упоминали, набор данных состоит из 327 680 цветных изображений, но мы собираемся использовать 10 000 случайно выбранных изображений из этого набора данных. Для этого сначала выберем 8000 индексов для обучающего набора и 2000 индексов для тестового набора.

# Selecting randomly the indexes from the dataset
np.random.seed(0)
train_imgs_orig = os.listdir(f"{base_dir}/train")
selected_image_list = []
for img in np.random.choice(train_imgs_orig, 10000):
    selected_image_list.append(img)

# Creating index variables for the training and test sets
np.random.shuffle(selected_image_list)
cancer_train_idx = selected_image_list[:8000]
cancer_test_idx = selected_image_list[8000:10000]

Отлично, мы создали индексные переменные для выбора изображений. Теперь давайте сохраним изображения в каталогах train и test, используя эти индексы.

# Saving the training images 
os.mkdir('cancer_train_dataset/')
for fname in cancer_train_idx:
    src = os.path.join(f'{base_dir}/train', fname)
    dst = os.path.join('cancer_train_dataset', fname)
    shutil.copyfile(src, dst)

# Saving the test images
os.mkdir('cancer_test_dataset/')
for fname in cancer_test_idx:
    src = os.path.join(f'{base_dir}/train', fname)
    dst = os.path.join('cancer_test_dataset/', fname)
    shutil.copyfile(src, dst)

Теперь давайте создадим фрейм данных Pandas, содержащий метки обучающего и тестового изображений.

# Creating a dataframe with labels
selected_image_labels = pd.DataFrame()
id_list = []
label_list = []

for img in selected_image_list:
    label_tuple = cancer_labels.loc[cancer_labels['id'] == img.split('.')[0]]
    id_list.append(label_tuple['id'].values[0])
    label_list.append(label_tuple['label'].values[0])

selected_image_labels['id'] = id_list
selected_image_labels['label'] = label_list

# Creating a variable in dictionary format to use data loading
img_class_dict = {k:v for k, v in zip(selected_image_labels.id, selected_image_labels.label)}

Отлично, мы выполнили этапы предварительной обработки данных. Мы готовы загрузить набор данных с помощью Lightning.

Как команде Uber удается упорядочивать свои данные и сплачивать команду? Отслеживание эксперимента кометы. Узнайте больше от Olcay Cirit от Uber.

Загрузка набора данных

Сложно управлять данными, которые будут использоваться для обучения модели. Он включает в себя все необходимые шаги для обработки, загрузки и преобразования данных, а также загрузчики данных для обучения, проверки, тестирования и прогнозирования.

В этом разделе мы увидим, как выполнить эти шаги с помощью Lightning DataModule, который поможет вам легко создавать модели, не зависящие от набора данных.

Обратите внимание, что мы не можем использовать этот модуль напрямую. Чтобы загрузить данные, нам сначала нужно создать наш собственный класс следующим образом:

# Creating the custom class to load the data
class LoadCancerDataset(Dataset):
    def __init__(self, datafolder, transform, labels_dict={}):
        self.datafolder = datafolder
        self.image_files_list = [s for s in os.listdir(datafolder)]
        self.transform = transform
        self.labels_dict = labels_dict
        self.labels = [labels_dict[i.split('.')[0]] for i in self.image_files_list]
    
    def __len__(self):
        return len(self.image_files_list)
    def __getitem__(self, idx):
        img_name = os.path.join(self.datafolder, self.image_files_list[idx])
        image = Image.open(img_name)
        image = self.transform(image)
        img_name_short = self.image_files_list[idx].split('.')[0]
        label = self.labels_dict[img_name_short]
        return image, label

Этот класс помогает нам прочитать все изображения в папке. Давайте продолжим и создадим класс DataModule для выполнения шагов обработки данных.

class CancerDataModule(pl.LightningDataModule):
    def __init__(self, batch_size, num_workers, data_dir):
        super().__init__()
        self.data_dir = data_dir
        self.batch_size = batch_size
        self.num_workers = num_workers
    
    # Preparing the dataset
    def prepare_data(self):
        """
        The prepare_data method was intentionally left empty as we have the dataset in our directory.
        """
        pass
     
    # Setuping the dataset
    def setup(self, stage=None):
        
        # Assigning train and val datasets to use in dataloaders
        if stage == "fit" or stage is None:            
            train_set_full = LoadCancerDataset(
                datafolder=f'{self.data_dir}/cancer_train_dataset',  
                transform=T.Compose([
                    T.Resize(224),
                    T.RandomHorizontalFlip(),
                    T.RandomVerticalFlip(),
                    T.ToTensor()
                ]),
                labels_dict=img_class_dict
            )
            train_set_size = int(len(train_set_full) * 0.9)
            valid_set_size = len(train_set_full) - train_set_size
            self.train_ds, self.val_ds = random_split(train_set_full, [train_set_size, valid_set_size])  

        # Assigning test dataset to use in dataloader    
        if stage == "test" or stage is None:            
            self.test_ds = LoadCancerDataset(
                datafolder=f'{self.data_dir}/cancer_test_dataset',
                transform=T.Compose([
                    T.Resize(224),
                    T.ToTensor()]),
                labels_dict=img_class_dict
            )
    # Creating the dataloaders
    def train_dataloader(self):
        return DataLoader(self.train_ds,batch_size=self.batch_size, 
                          num_workers=self.num_workers,shuffle=True)

    def val_dataloader(self):
        return DataLoader(self.val_ds,batch_size=self.batch_size, 
                          num_workers=self.num_workers,shuffle=False)

    def test_dataloader(self):
        return DataLoader( self.test_ds,batch_size=self.batch_size, 
                          num_workers=self.num_workers,shuffle=False)

Обратите внимание, что если вы используете чистый PyTorch для кодирования этих шагов, вам нужно написать больше кода. Выполнить эти шаги с помощью Lightning DataModule совсем несложно.

Отлично, мы создали класс для управления данными. Перейдем к построению модели.

Построение модели с помощью Lightning

На данный момент мы подготовили данные, которые будем использовать для обучения модели. В этом разделе мы собираемся создать архитектуру нашей модели для классификации изображений. Для этого мы собираемся использовать технику трансферного обучения.

Перенос обучения – это метод, который позволяет нам использовать предварительно обученную модель. Что нам нужно сделать, так это адаптировать предварительно обученную модель для нашей задачи. Предварительно обученная модель, которую мы собираемся использовать, — это архитектура ResNet-50. Эта архитектура часто используется для классификации изображений. Он состоит из 50 слоев, содержащих сверточные слои, объединяющие слои и полносвязные слои.

Теперь мы собираемся использовать LightningModule для построения нашей модели ResNet. Этот модуль помогает нам организовать наш код PyTorch и легко управлять этапами обучения, проверки и тестирования.

class CancerImageClassifier(pl.LightningModule):

    def __init__(self, learning_rate = 0.001, num_classes = 2):   
        super().__init__()
        self.learning_rate = learning_rate
        self.loss_fn = nn.CrossEntropyLoss()   
        self.num_classes = num_classes
        # Defining metrics
        self.accuracy = Accuracy(task="binary", num_classes=num_classes)  
        self.f1_score = F1Score(task="binary", num_classes=num_classes)
        self.history = {'train_loss':[],'train_acc':[],'val_loss':[],'val_acc' : []}
        
        # Defining the model architecture
        self.pretrain_model = resnet50(weights=ResNet50_Weights.DEFAULT)
        self.pretrain_model.eval()
        for param in self.pretrain_model.parameters():
            param.requires_grad = False
        
        self.pretrain_model.fc = nn.Sequential(
            nn.Linear(self.pretrain_model.fc.in_features, 1024),
            nn.ReLU(),
            nn.Dropout(),
            nn.Linear(1024,self.num_classes)
        )
    # To run data through the model
    def forward(self, input):
        output=self.pretrain_model(input)
        return output

    def training_step(self, batch, batch_idx):       
        outputs, targets, loss, preds = self._common_step(batch, batch_idx)     
        train_accuracy = self.accuracy(preds, targets)           
        self.history['train_loss'].append(loss.item())
        self.history['train_acc'].append(train_accuracy.item())      
        self.log_dict(
            {"train_loss": loss,"train_acc": train_accuracy,},
            on_step=False, on_epoch=True, prog_bar=True)  
        return {"loss":loss, 'train_acc': train_accuracy}
            
    def validation_step(self, batch, batch_idx):      
        outputs, targets, loss, preds = self._common_step(batch, batch_idx)  
        val_accuracy = self.accuracy(preds, targets)     
        self.history['val_loss'].append(loss.item())
        self.history['val_acc'].append(val_accuracy.item())     
        self.log_dict(
            {"val_loss": loss,"val_acc": val_accuracy},
            on_step=False, on_epoch=True, prog_bar=True,
        )
        return {"loss":loss, 'val_acc': val_accuracy}
    
    def test_step(self, batch, batch_idx):
        outputs, targets, loss, preds = self._common_step(batch, batch_idx)
        test_accuracy = self.accuracy(preds, targets)           
        f1_score = self.f1_score(preds, targets)         
        self.log_dict(
            {"test_loss": loss,
             "test_acc": test_accuracy, 
             "test_f1_score": f1_score},
            on_step=False, on_epoch=True, prog_bar=True,
        )
        return {"test_loss":loss, 
                "test_accuracy":test_accuracy, 
                "test_f1_score": f1_score}
    
    def _common_step(self, batch, batch_idx):
        inputs, targets = batch
        outputs = self.forward(inputs)
        loss = self.loss_fn(outputs, targets)
        preds = torch.argmax(outputs, dim=1)
        return outputs, targets, loss, preds

    def configure_optimizers(self):
        params = self.parameters()
        optimizer = optim.Adam(params=params, lr = self.learning_rate)
        return optimizer

Как видите, код, используемый для построения модели, более понятен и его легче отслеживать, чем чистые коды PyTorch.

Одна из моих любимых особенностей Lightning — простота отслеживания показателей. Здесь мы использовали метод self.log_dict для регистрации метрик на этапах обучения и тестирования.

Итак, мы создали архитектуру нашей модели. Давайте продолжим и посмотрим, как отслеживать наши гиперпараметры, метрики и модель.

Отслеживание с помощью Comet Logger

У меня для вас отличные новости. Lightning поддерживает платформу Comet ML. Это означает, что вы можете отслеживать гиперпараметры модели, отслеживать показатели модели и регистрировать свою модель в своей учетной записи Comet ML.

В Lightning есть класс CometLogger, который можно использовать для беспрепятственной регистрации метрик, гиперпараметров, весов моделей и многого другого. Просто создайте экземпляр объекта CometLogger и передайте его в Trainer Lightning. Давай сделаем это.

# Creating an experiment with your API key
comet_logger = CometLogger(
    api_key= "your_api_key",
    workspace="your_workspace",
    project_name="your_project_name"
)

Отлично, теперь у нас есть объект для отслеживания гиперпараметров и метрик. Что нам нужно сделать, так это передать этот объект в Trainer , как показано ниже. Теперь мы готовы обучить модель.

Обучение модели

После организации нашего кода PyTorch в LightningModule Trainer выполняет все остальное автоматически. Обратите внимание, что объект Trainer предлагает гораздо больше, чем «обучение». Это помогает нам устанавливать такие параметры, как выбор аппаратного обеспечения (ЦП/ГП/ЦП), периоды обучения и тестирования, регистрация комет, поддержка 16-битного обучения и так далее.

Прежде чем создавать объект тренера, давайте создадим экземпляр объекта DataModule следующим образом:

my_dataloader = CancerDataModule(
        batch_size=128,
        num_workers=2,
        data_dir="./")

После этого создадим объект модели из класса CancerImageClassifier.

my_model = CancerImageClassifier(
        num_classes=2,
        learning_rate=0.001)

Далее создадим экземпляр объекта Trainer, а затем вызовем метод fit для обучения модели.

my_trainer = pl.Trainer(
        logger=comet_logger,
        accelerator="auto",
        devices="auto",
        max_epochs=15)

my_trainer.fit(my_model, my_dataloader)

Когда мы запускаем эти коды, начинается процесс обучения. Вы можете отслеживать показатели на панели управления Comet ML следующим образом:

На данный момент мы обучили нашу модель и изучили ее показатели, такие как точность, потери и оценка F1, на панели управления Comet ML. Перейдем к оценке модели.

Оценка модели

Теперь у нас есть хорошая модель для классификации изображений, но как наша модель работает на тестовом наборе? Чтобы выяснить это, мы можем использовать объект Trainer, вызвав тестовый метод.

# Model evaluation
my_dataloader.setup()
my_trainer.test(model=my_model, dataloaders=my_dataloader.test_dataloader())

Как видите, метрики на тестовом наборе близки к метрикам на обучающем наборе. Наконец, давайте сохраним нашу модель в Comet ML и завершим наш эксперимент следующим образом:

# Saving Model in Comet-ML
from comet_ml.integration.pytorch import log_model
log_model(comet_logger.experiment, my_model, model_name="my_pl_model")

# Ending our experiment
comet_logger.experiment.end()

Создание приложения с Gradio

Gradio – это библиотека Python, позволяющая создавать демонстрационные приложения. После обучения модели вы можете использовать эту библиотеку для создания простого веб-интерфейса, доступного для просмотра любому пользователю. Во-первых, мы собираемся определить функцию для предсказания метки изображения.

# Defining a function to predict the label of an image
def predict(inp):
    image_transform = transforms.Compose([ transforms.Resize(size=(224,224)), transforms.ToTensor()])
    labels = ['normal', 'cancer']
    inp = image_transform(inp).unsqueeze(dim=0)
    with torch.no_grad():
        prediction = torch.nn.functional.softmax(model(inp))
        confidences = {labels[i]: float(prediction.squeeze()[i]) for i in range(len(labels))}    
    return confidences

Далее давайте создадим интерфейс Gradio, используя следующие коды.

# Creating a Gradio interface
gr.Interface(fn=predict, 
             inputs=gr.Image(type="pil"),
             outputs=gr.Label(num_top_classes=2),
             title=title,
             description=description,
             article=article,
             examples=['image-1.jpg', 'image-2.jpg']).launch()

Вот и все! Наше приложение готово и выглядит так:

Используя это приложение, вы можете предсказать, является ли изображение метастатической ткани раковым.

Обратите внимание, что вы можете найти файлы, которые я использовал для создания этого приложения, здесь.

Заворачивать

Когда дело доходит до реализации сложных моделей глубокого обучения, вы сталкиваетесь со многими проблемами. К счастью, вы можете решить эти проблемы с помощью современных библиотек, таких как Lightning и Comet ML.

В этом блоге мы узнали, как реализовать комплексный проект глубокого обучения с использованием современных библиотек. Lightning позволяет нам легко упорядочивать коды PyTorch для построения моделей. Класс CometLogger в Lightning помогает нам отслеживать показатели нашей модели. Gradio позволяет нам создать приложение для обнаружения рака.

Вот и все. Спасибо за прочтение. Подключим YouTube | Твиттер | ЛинкедИн.





Ресурсы

Примечание редактора. Heartbeat — это интернет-издание и сообщество, созданное участниками и посвященное предоставлению лучших образовательных ресурсов для специалистов по науке о данных, машинному обучению и глубокому обучению. Мы стремимся поддерживать и вдохновлять разработчиков и инженеров из всех слоев общества.

Независимая от редакции, Heartbeat спонсируется и публикуется Comet, платформой MLOps, которая позволяет специалистам по данным и командам машинного обучения отслеживать, сравнивать, объяснять и оптимизировать свои эксперименты. Мы платим нашим авторам и не продаем рекламу.

Если вы хотите внести свой вклад, перейдите к нашему призыву к участию. Вы также можете подписаться на получение нашего еженедельного информационного бюллетеня (Еженедельник глубокого обучения), заглянуть в блог Comet, присоединиться к нам в Slack и подписаться на Comet в Twitter и LinkedIn для получения ресурсов и событий. и многое другое, что поможет вам быстрее создавать более качественные модели машинного обучения.