Пошаговое руководство по использованию пакета концентратора для спутниковых снимков

Введение

Инженеры по машинному обучению меньше работают над машинным обучением и больше над подготовкой данных. Фактически, типичный инженер машинного обучения тратит более 50% своего времени на предварительную обработку данных, а не на их анализ. Подготовка конвейеров данных, чтобы команда инженеров могла использовать данные, занимает много времени, что делает проведение экспериментов по машинному обучению и совместную работу в команде довольно сложной задачей.

Моя команда инженеров по машинному обучению и я недавно столкнулись с аналогичными проблемами при работе над проектом с Omdena, а также с Институтом мировых ресурсов. К счастью, нас поддержала команда Activeloop и их Центр пакетов с открытым исходным кодом, что упростило для нашей группы сотрудников работайте одновременно над проведением экспериментов и намного быстрее достигайте конечной цели. Вы можете рассматривать Activeloop’s Hub как Docker Hub для наборов данных.

Заявление о проблеме

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

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

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

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

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

Ограничением было использование только данных из открытых источников, поэтому мы решили использовать Google Earth Engine для загрузки изображений со спутника Landsat 8.

Мы создадим задачу классификации изображений, где входными данными будут спутниковые изображения районов, а выходными данными будет индекс богатства активов. Ограничением было использование только данных из открытых источников, поэтому мы решили использовать Google Earth Engine для загрузки изображений со спутника Landsat 8.

Затем возникла настоящая проблема, с которой мы столкнулись, проблемы с хранением данных.

Спутниковые изображения, в отличие от изображений, которые мы храним в наших устройствах, являются «многополосными». Они не просто содержат полосы RGB, но могут содержать до 12 полос, некоторые из которых относятся к ближним инфракрасным диапазонам, коротковолновым инфракрасным диапазонам и т. Д. Это приводит к очень большим изображениям, когда одно изображение районного уровня содержит 12 полос. может занимать до 150 МБ. Это делает хранение этих изображений и их хранение таким образом, чтобы каждый соавтор, работающий над этой задачей, мог получить к ним доступ и проводить над ними эксперименты, что является действительно сложной задачей.

Существуют несколько методов решения этой проблемы:

  • Google Диск можно использовать для хранения данных и использования Google Collaboratory для всех экспериментов по моделированию. Однако, когда дело доходит до спутниковых снимков, часто в стандартной учетной записи Google Диска не хватает места.
  • Облачные средства - хороший, но дорогой вариант. Фактически вы можете проверить, сколько денег вы можете потерять, используя такие средства, с помощью этого инструмента, созданного Activeloop.

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

На помощь приходит центр пакетов с открытым исходным кодом Activeloop!

Простое хранение неструктурированных данных с помощью концентратора

Пакет python hub может использоваться для хранения неструктурированных данных (в данном случае - аэрофотоснимков) на платформе Activeloop, он может быть легко загружен кем угодно в любом месте, используя буквально одну строку кода, все, что нам нужно для do - это упоминание зарегистрированного имени учетной записи и имени набора данных.

dataset.load(“arpan/district-awi-2015–16-rgb-custom”)

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

Чтобы настроить концентратор в вашей системе, просто выполните следующие команды:

pip install hub==0.11.0
hub register 
hub login

После этого вы можете войти в систему, используя свои учетные данные. Обратите внимание, что в то время я использовал концентратор версии 0.11.0, однако теперь доступна бета-версия 1.0.0b1 с гораздо лучшим интерфейсом для конвейеров данных и всесторонней поддержкой хранения наборов данных. Пакет теперь является самым быстрым способом доступа и управления наборами данных для PyTorch и TensorFlow.

Теперь я перейду к коду, который я использовал для загрузки данных с помощью hub. Для загрузки спутниковых изображений их сначала необходимо загрузить из движка Google Earth, мы выбрали GEE, так как нам пришлось использовать общедоступные спутниковые данные. Я скачал растровые изображения размером 20 х 20 км для каждого района.

Создается класс генератора для загрузки изображений с помощью хаба. Он состоит из двух основных методов: мета и пересылки. Для любого файла, который мы хотим загрузить, нам нужно указать, какие данные он содержит. Это обрабатывается мета-методом.

Мета-метод содержит описание каждого файла в виде словаря. В нашем случае так выглядит мета-функция.

from hub import Transform, dataset
class AwiGenerator(Transform):
 def meta(self):
 return {
 ‘rgb-image’: {“shape”: (1,), “dtype”: “object”, ‘dtag’: ‘image’},
 ‘custom-image’: {“shape”: (1,), “dtype”: “object”, ‘dtag’: ‘image’},
 ‘awi’: {‘shape’: (1,), ‘dtype’: ‘float’, ‘dtag’: ‘text’},
 ‘target’: {‘shape’: (1, ), ‘dtype’: ‘float’, ‘dtag’: ‘text’},
 ‘name’: {‘shape’: (1, ), ‘dtype’: ‘object’, ‘dtag’: ‘text’}
 }

Словарь описания содержит 3 вещи:

  • shape - это форма 1 файла данного типа. Например, «rgb-изображение» относится к изображению, созданному с использованием красных, зеленых и синих полос, форма для каждого изображения будет 1 x высота x ширина x 3 (в форме массива или тензора). Мы можем просто использовать соглашение numpy и упомянуть (1,) как фигуру.
  • dtype - это тип данных файла. Изображения и названия (название района) хранятся как объекты, а awi (индекс богатства активов) и цель как плавающие объекты.
  • dtag. Об этом следует упомянуть, если мы хотим использовать приложение-визуализатор, предоставляемое Activeloop. После того, как изображения были загружены, мы фактически можем видеть все изображения вместе с предоставленными нами метками (цель, имя и т. Д.). Мы увидим, как выглядели наши загрузки, позже в этой статье.

Прежде чем мы перейдем к прямому методу, нам нужно немного понять, какой именно была архитектура модели. Спутниковые изображения многополосные, Landsat 8, наш интересующий спутник содержит 12 полосных изображений. Более подробную информацию можно найти в каталоге данных Google Earth Engine. Для нашей модели нам требуются красный, зеленый, синий, ближний инфракрасный и коротковолновый инфракрасные диапазоны.

Используя диапазоны NIR и SWIR, мы можем рассчитать растительность (NDVI), застроенную (NDBI) и водные индексы (NDWI) из растров. Эти индексы выделяют различные географические атрибуты региона, для которого у нас есть изображение. Например, NDVI выделяет все участки растительности на изображении, увеличивая значения пикселей зеленого цвета. Аналогичным образом NDWI выделяет водные объекты синим цветом, а NDBI выделяет застроенные территории (здания, дороги и т. Д.), Выделяя белый цвет.

Используя диапазоны NIR и SWIR, мы можем рассчитать растительность (NDVI), застроенные участки (NDBI) и водные индексы (NDWI).

Эти 3 индекса представляют собой не что иное, как однополосные изображения. Затем мы объединяем эти 3 изображения с одной полосой в одно изображение с тремя полосами, чтобы модель могла учиться на основе всех трех этих индексов. RGB, а также изображение Custom Band являются входными данными для модели.

В итоге мы получаем такую ​​архитектуру:

Целевой показатель - это категория AWI, которая варьируется от 0 до 4 (0 - очень бедные, 4 - очень богатые). Он был рассчитан путем дискретизации значений AWI. Мы решили преобразовать это в проблему классификации, объединяя значения AWI, поскольку AWI имеет очень высокую дисперсию, поскольку у нас есть только около 640 изображений для моделирования, мы решили, что подход классификации будет лучше.

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

import rasterio
def forward(self, image_info):

        """ 
        Returns a dictionary containing the files mentioned in 
        meta for every image path

        :params image_info: list of dictionaries each containing  
         image path, target, names and awi
  
       """
        ds = {}
        
        # open the satellite image 
        image =  rasterio.open(image_info['image_path'])
        
        # get the image inputs (RGB + CUSTOM)        
        _,_,_,rgb,comb = self.get_custom_bands(self, image)
        
        # initialize the placeholders for the data and store them
        ds['rgb-image'] = np.empty(1, object)
        ds['rgb-image'][0] = rgb

        ds['custom-image'] = np.empty(1, object)
        ds['custom-image'][0] = comb
        
        ds['awi'] = np.empty(1, dtype = 'float')
        ds['awi'][0] = image_info['awi']

        ds['name'] = np.empty(1, dtype = object)
        ds['name'][0] = image_info['name']

        ds['target'] = np.empty(1, dtype = 'float')
        ds['target'][0] = image_info['target']

        return ds

Спутниковые изображения не сохраняются в системе в наших форматах .jpg или .png, они имеют специальный формат, называемый форматом GeoTiff, с расширением .tif. Чтобы получить доступ к такому файлу, я использовал rasterio, пакет Python для чтения, визуализации и преобразования спутниковых изображений.

Метод get_custom_band вычисляет индексы NDVI, NDBI, NDWI, объединяет их и возвращает RGB и пользовательские изображения с полосами. Несмотря на то, что понимание кода этого метода не является нашей целью в этой статье, вы все равно можете посмотреть ниже:

    @staticmethod
    def get_custom_bands(self, image):
        """ Returns custom 3 banded image by  combining  NDVI/NDBI/NDWI 
"""
            
        # NDBI = (SWIR - NIR) / (SWIR + NIR) 
        nir = image.read([5])
        nir = np.rollaxis(nir, 0, 3)

        swir = image.read([6])
        swir = np.rollaxis(swir, 0, 3)

        # Do not display error when divided by zero 
        np.seterr(divide='ignore', invalid='ignore')
        
        ndbi = np.zeros(nir.shape)  
        ndbi = (swir-nir) / (swir+nir)
        ndbi = cv2.normalize(ndbi, None, alpha = 0, beta = 255, norm_type = cv2.NORM_MINMAX, dtype = cv2.CV_32F)
        ndbi = ndbi[:,:,np.newaxis]
                    
        
        # RGB
        rgb =  image.read([4,3,2])          
        rgb = cv2.normalize(rgb, None, alpha = 0, beta = 255, norm_type = cv2.NORM_MINMAX, dtype = cv2.CV_32F).transpose(1,2,0)
        rgb = np.where(np.isnan(rgb), 0, rgb)
        assert not np.any(np.isnan(rgb))
                
        # NDVI = (NIR - Red) / (NIR + Red)    
        red = image.read([4])
        red = np.rollaxis(red, 0, 3)
        ndvi = np.zeros(nir.shape)
        ndvi = (nir-red)/(nir+red)
        ndvi = cv2.normalize(ndvi, None, alpha = 0, beta = 255, norm_type = cv2.NORM_MINMAX, dtype = cv2.CV_32F)
        ndvi = ndvi[:,:,np.newaxis]
        
        # NDWI = (NIR - SWIR) / (NIR + SWIR)    
        ndwi = (nir-swir)/(nir+swir)
        ndwi = cv2.normalize(ndwi, None, alpha = 0, beta = 255, norm_type = cv2.NORM_MINMAX, dtype = cv2.CV_32F)
        ndwi = ndwi[:,:,np.newaxis]
        
        # combined     
        comb = np.concatenate([ndbi, ndvi, ndwi], axis = -1)    
        comb = np.where(np.isnan(comb), 0, comb)
        assert not np.any(np.isnan(comb))
        

        return ndbi, ndvi, ndwi, rgb, comb

Другой подход здесь мог заключаться в загрузке всех диапазонов, то есть диапазонов красного, зеленого, синего, SWIR и NIR вместо загрузки отдельных диапазонов RGB и пользовательских. Во время моделирования, когда мы загружаем изображения, мы могли затем рассчитать необходимые входные данные, используя наш метод get_custom_bands.

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

Теперь, когда мы создали класс, следующим шагом будет создание словаря image_info, который содержит все, что мы хотим загрузить, то есть путь к изображению, из которого мы будем получать изображения rgb и пользовательские изображения, файл awi , цель и название районов. Затем мы создаем объект набора данных с помощью метода набора данных, предоставленного hub, и класса AWiGenerator, созданного выше.

Об этом позаботится функция загрузки набора данных:

def load_dataset(raster_path):
    """
    Creates the dataset object used to upload the data.

    :param raster_path: The path where all the rasters are stored    in the system
    """
    # get the train path and the test path
    train_path = os.path.join(raster_path, 'Train')
    test_path = os.path.join(raster_path, 'Test')

    # dataframe containing target
    df = pd.read_csv('Data - Full/data-class.csv')
    train_image_paths = os.listdir(train_path)
    test_image_paths = os.listdir(test_path)

    # initialize image info list
    image_info_list = []

    # iterate over all images in train and test
    tk_train = tqdm(train_image_paths, total = len(train_image_paths))
    tk_test = tqdm(test_image_paths, total = len(test_image_paths))

    # there are 543 training images
    for image in tk_train:
        # image info dictionary for each image
        image_info = {}

        # store the image path
        image_info['image_path'] = os.path.join(train_path, image)
        # getting the awi and name from raster name
        awi = float(image.split('_')[1][:-4])        
        name = str(image.split('_')[0])
        image_info['awi'] = awi
        image_info['name'] = name
        
        # get the target for the image
        target = df['category'][(df['distname'] == name) & (df['wealth_ind'] == awi)]
        image_info['target'] = target

        # check if the image is corrupted, if it's not then append  
        try:
            image = rasterio.open(image_info['image_path'])
            image_info_list.append(image_info)
        
        except Exception as e:
            print(e)
            print('Image not found')

Мы выполняем тот же процесс и для тестовых данных. Наконец, мы получаем список словарей image_info_list, в котором первые 543 словарей являются обучающими, а остальные соответствуют тестируемому набору.

Когда список готов, мы просто вызываем команду generate и возвращаем объект набора данных.

ds = dataset.generate(AwiGenerator(), image_info_list)
return ds

Все настройки, необходимые для загрузки данных, выполнены! Теперь нам просто нужно инициализировать наш объект набора данных, назвать его и загрузить с помощью команды store.

path = 'Data - Full/'
ds = load_dataset(path)
# name of the data
ds.store('district-awi-2015-16-rgb-custom')

Готово! Данные будут загружены в вашу зарегистрированную учетную запись с указанным именем. После того, как набор данных был загружен, мы можем фактически увидеть все, что было загружено с помощью приложения визуализатора.

Вот как выглядят изображения RGB с их названиями.

Мы также можем видеть каждый из них по отдельности, вот изображение в формате rgb Бангалора и пользовательское изображение с его целевым значением.

Вы можете поиграть со всем набором данных здесь. Этот инструмент помог мне практически мгновенно визуализировать любой фрагмент набора данных с помощью больших спутниковых изображений. Это было удобно для выявления изображений с ошибками и их удаления. Ряд популярных наборов данных, таких как mnist, fashion-mnist или CoCo, также предварительно загружен в инструмент визуализации.

Обучение моделей машинного обучения с помощью концентратора

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

Чтобы загрузить набор данных, мы просто используем функцию загрузки.

import hub

# load 2015-16 awi data 
ds = hub.load("arpan/district-awi-2015-16-rgb-custom")

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

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

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

import albumentations
# imagenet stats
mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]

# training augmentations 
aug = albumentations.Compose([
    albumentations.Resize(512, 512, always_apply=True),            
    albumentations.Normalize(mean, std, always_apply = True),
    albumentations.RandomBrightnessContrast(always_apply =   False),
    albumentations.RandomRotate90(always_apply=False),
    albumentations.HorizontalFlip(),
    albumentations.VerticalFlip()])
# testing augmentations
aug_test = albumentations.Compose([
    albumentations.Resize(512, 512, always_apply=True),
    albumentations.Normalize(mean, std, always_apply = True)])

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

Для этого мы создадим такой класс трансформатора:

class TrainTransformer(Transform):
    def meta(self):
       return {
           'rgb-image': {"shape": (1,), "dtype": "object", 'dtag': 'image'},            
           'custom-image': {"shape": (1,), "dtype": "object", 'dtag': 'image'},                    
           'target': {'shape': (1, ), 'dtype': 'float', 'dtag': 'text'}          
        }

    def forward(self, item):
        ds = {}
        
        # load rgb and apply augmentations
        ds['rgb-image'] = np.empty(1, object)
        rgb = item['rgb-image']          
        ds['rgb-image'][0] = aug(image = rgb)['image'].transpose(2,0,1)
        
        # load custom and apply augmentations
        ds['custom-image'] = np.empty(1, object)
        custom = item['custom-image']       
        ds['custom-image'][0] = aug(image = custom)['image'].transpose(2,0,1)
# load the target
        ds['target'] = np.empty(1, dtype = 'float')
        ds['target'][0] = item['target']
        return ds

Это очень похоже на класс генератора, который мы создали для загрузки изображений. У нас есть мета-метод, который определяет все файлы, которые мы хотим использовать для обучения (мы не используем name и awi, поскольку они не требуются), и у нас есть прямой метод, который применяет дополнения к нашим изображениям одно за другим и возвращает данные в форма словаря.

Мы выполняем те же самые шаги, чтобы создать TestTransformer.

Теперь нам нужно инициализировать наш набор данных для обучения и тестирования и преобразовать их в формат PyTorch или TensorFlow. Я использовал Fast AI 2.0, который построен поверх PyTorch для обучения моделей, и поэтому я преобразовал набор данных в формат PyTorch. FastAI также предоставляет некоторые расширенные функции, такие как поиск оценщика обучения, постепенное размораживание, различительная скорость обучения и т. Д. Обычно для их реализации в PyTorch требуется гораздо больше кода. FastAI + Hub, проводить эксперименты с машинным обучением еще никогда не было так просто!

# --------------------------------------------------------
num_train_samples = 543
train_ds = dataset.generate(TrainTransformer(), ds[0:num_train_samples])
test_ds = dataset.generate(TestTransformer(), ds[num_train_samples:])
# --------------------------------------------------------
# convert to pytorch
train_ds = train_ds.to_pytorch(lambda x:((x['rgb-image'], x['custom-image']), x['target']))
test_ds = test_ds.to_pytorch(lambda x:((x['rgb-image'], x['custom-image']), x['target']))
# dataloaders
train_loader = torch.utils.data.DataLoader(train_ds, batch_size = 10,shuffle=True)
test_loader = torch.utils.data.DataLoader(test_ds, batch_size = 10,shuffle=False)

Когда мы загрузили набор данных, мы увидели, что первые 543 выборки были точками обучающих данных, поэтому переменная num_train_samples принимает это значение.

После создания набора данных мы используем нашу стандартную оболочку загрузчика данных PyTorch для создания обучающих и тестовых загрузчиков. Теперь мы готовы создать нашу модель.

Сначала мы создаем архитектуру нашей модели, которая использует 2 экстрактора функций resnet 18 (тело), ​​а затем полностью связанный слой (голова).

class AWIModel(nn.Module):
  def __init__(self,arch, ps = 0.5, input_fts = 1024):
    super(AWIModel, self).__init__()
     
    # resnet 18 feature extractors
    self.body1 = create_body(arch, pretrained=True)
    self.body2 = create_body(arch, pretrained=True)
    # fully connected layers
    self.head = create_head(2*input_fts, 5)

  def forward(self, X):
    x1, x2 = X
    x1 = self.body1(x1)
    x2 = self.body2(x2)    
    x = torch.cat([x1,x2], dim = 1)        
    x = self.head(x)

    return x

Затем мы определяем нашу функцию потерь, объект Dataloaders fastai и создаем наш обучающий объект.

dls = DataLoaders(train_loader, test_loader)
model = AWIModel(arch = models.resnet18, ps = 0.2)
learn = Learner(dls = dls, model = model, loss_func = loss_fnc, opt_func = Adam, metrics = [accuracy], cbs = CudaCallback(device = 'cuda'))

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

learn.fit_one_cycle(6, lr_max =  5e-4)

Вывод: самое быстрое решение для эффективного машинного обучения и масштабируемых конвейеров данных

Мы прошли весь конвейер машинного обучения, который можно создать с помощью пакета hub. Данные хранятся в централизованном хранилище, чтобы команда инженеров по машинному обучению могла легко использовать их для загрузки данных и проведения экспериментов с ними. Примечательно, что к данным можно получить доступ и изменить так же быстро, как если бы это было локально. Наборы данных, для загрузки и подготовки которых в противном случае потребовалось бы 30–40 часов, для доступа к ним с помощью hub потребуется 2 минуты. После загрузки все наборы данных изображений могут быть визуализированы, что позволит провести некоторое исследование и отладку. Все это сэкономило моей команде около 2 недель времени, что огромно для проекта с выделенным временем в 8 недель.

В целом, решение Activeloop с открытым исходным кодом создано для распределенных команд, которым необходимо быстро получать результаты при минимальных затратах. Спутниковые снимки - это лишь верхушка айсберга. Стек с открытым исходным кодом имеет гораздо больше функций для масштабируемых конвейеров данных и более простого управления наборами данных. Например, с новым обновлением v1.0 хранение данных, а также предварительная обработка наборов данных станут еще проще. Посетите их веб-сайт и документацию, чтобы узнать больше, и присоединитесь к Сообществу Slack Activeloop, чтобы задать команде больше вопросов!