С августа 2019 года по июль 2020 года было уничтожено в общей сложности 11 088 кв. км (4 281 кв. миль) тропических лесов. Это на 9,5% больше, чем годом ранее. Тропические леса Амазонки, покрывающие 5,5 миллионов квадратных километров, являются жизненно важным хранилищем углерода, замедляющим темпы глобального потепления. Однако он также подвержен лесным пожарам и вырубке лесов. Учитывая все разрушительные последствия вырубки лесов, важно лучше понять лесной ландшафт, чтобы заинтересованные лесные организации могли знать, подвержен ли лесной массив вырубке или нет, и принимать необходимые превентивные меры.

Спутниковые снимки тропических лесов Амазонки доступны в рамках этого конкурса kaggle. Он содержит спутниковые снимки бассейна Амазонки с разрешением 3–5 метров, которые имеют несколько меток из 17 категорий, которые дополнительно сгруппированы/разделены на 3 мета-категории:

  1. Ярлык "Атмосфера":облачно, переменная облачность, дымка, ясно
  2. Общие земли — первичные (первичные тропические леса), вода (водоемы, такие как реки и озера), сельское хозяйство, жилье (основанные города), культивирование, редкая метка, дорога
  3. Редкая земля –продувка, рубящий ожог, цветение, голая земля, кустарная шахта, классическая шахта.

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

Каждый из этих сегментов имеет одну или несколько из 17 меток, связанных с ними.

Исследовательский анализ данных

Данные о тропических лесах содержат 40 479 изображений сегментированных спутниковых снимков с 116 278 метками на изображениях. Таким образом, в среднем с изображением связано 2,87 ярлыка. На приведенном ниже графике показано распределение меток для всех изображений в наборе данных.

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

Матрица совпадения A показывает, что большинство меток перекрываются. Матрица совпадения B показывает нам, что только одна атмосферная метка помечена для изображения (без совпадения). Что логично, потому что в лесной зоне не может быть пасмурно и пасмурно одновременно. Но несколько меток земли могут быть связаны с ландшафтом, таким как сельское хозяйство, вода и т. д., что показано в матрице совместной встречаемости C.

Классификационный подход

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

Многоуровневая классификация

Этот классификатор выводит прогноз для каждой из 17 предоставленных меток. Здесь мы предполагаем, что модель не снабжена информацией о метакатегории, поэтому она рассматривает все свои метки как одну группу. Он имеет 17 бинарных перекрестных энтропийных головок, каждая из которых выводит вероятностное значение (в диапазоне от 0 до 1) в качестве предсказания, которое не зависит друг от друга. Ниже приведен код для создания этого классификатора.

"""
import required keras dependencies... 
"""
_NUM_CLASSES = 17
### After building a model or just using an existing pre-trained 
### model(in this case VGG16). We have an output layer, ###'output_layer' with size 17 matching the total number of labels 
all_labels_x = Dense(64,activation = 'relu',name  = 'rat_fc3')(x)
prediction_all = Dense(_NUM_CLASSES, activation = 'hard_sigmoid', name = "prediction_ratings")(new_x)
multi_label_loss_model = Model(inputs = model_input, outputs = prediction_al)
multi_loss_model.compile(optimizer= 'ADAM', loss ='binary_crossentropy',metrics = [metrics.Precision(),metrics.Recall(),metrics.binary_accuracy])

Многоотраслевой классификатор с несколькими метками

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

"""
import required keras dependencies...
"""
### After building a model or just using an existing pre-trained 
### model(in this case VGG16).Now instead of one output later of ###length 17, we have three different output layers connected to the ###first flattening layer. 
_NUM_CLASSES_0, _NUM_CLASSES_1, _NUM_CLASSES_2 = 4,6,7
## branching starts....
atmospheric_x = Dense(256,activation = 'relu',name  = 'rtype_fc1')(x)
common_x = Dense(256,activation = 'relu',name  = 'rat_fc1')(x)
rare_land_x = Dense(256,activation = 'relu',name  = 'rare_fc1')(x)

## 3 different binary heads for 3-meta categories
prediction_atmos = Dense(_NUM_CLASSES_0, activation = 'hard_sigmoid',name = "prediction_atmos")(atmospheric_x)
prediction_common = Dense(_NUM_CLASSES_1, activation = 'hard_sigmoid', name = "prediction_common")(common_x)
prediction_rare = Dense(_NUM_CLASSES_2, activation = 'hard_sigmoid',name = "prediction_rare")(rare_land_x)

### Classifying from all these branches
multi_loss_model = Model(inputs = model_input, outputs = [prediction_atmos, prediction_common, prediction_rare] )
multi_loss_model.compile(optimizer= optimizer, loss = {'prediction_atmos':'binary_crossentropy', 'prediction_common' : 'binary_crossentropy', 'prediction_rare' : 'binary_crossentropy', },metrics = {"prediction_atmos":[metrics.Precision(),metrics.Recall(),metrics.binary_accuracy], "prediction_common":[metrics.Precision(),metrics.Recall(),metrics.binary_accuracy], "prediction_rare":[metrics.Precision(),metrics.Recall(),metrics.binary_accuracy]})

Подробности эксперимента

Обе модели нейронной сети были обучены на полном наборе данных. Набор данных был разделен на обучающий, тестовый и проверочный наборы с разделением 60% — 20% — 20%. Так как обучающих примеров было много, я решил использовать трансферное обучение, а не обучать всю сеть с нуля. Поэтому я заморозил все веса сверточных слоев и разморозил веса полностью подключенных слоев. Я провел несколько экспериментов с гиперпараметрами, такими как эпохи (10, 25, 50), размер партии (32, 63, 128), скорость обучения оптимизатора (10–3, 10–4, 10–5, 10–6), слои заморожены (без обновления веса) (0, 8, 16), оптимизаторы (SGD, ADAM, ADA_DELTA).

Конвейер ETL для пакетного обучения

Поскольку данные были слишком большими для загрузки для пакетного обучения, мне пришлось сначала закодировать их в двоичный формат tensorflow, называемый TFrecords, декодировать их обратно в пакетах, применить необходимые преобразования предварительной обработки и загрузить их в модель, используя tensorflow. Набор данных () API. ПРИМЕЧАНИЕ. В tf 2.0 вам больше не нужно кодировать данные в tfrecords. Код ниже показывает эти шаги

import cv2
import matplotlib.pyplot as plt
import tensorflow as tf   # only work from tensorflow==1.9.0-rc1 and after
"""
************** To encode your data into TFrecords format ****************
"""
## Image and labels datatype conversion helper function
def wrap_int64(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=list(value.reshape(-1))))
def wrap_float32(value):
return tf.train.Feature(float_list=tf.train.FloatList(value= list(value)))
def wrap_bytes(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value = [value]))
def convert_2labels(image_path, labels, out_path):
    with tf.python_io.TFRecordWriter(out_path) as writer:
        for i, (path, label) in enumerate(zip(image_path, labels)):
            try:
                image = plt.imread(path)
                image = cv2.resize(image, (224, 224))
                size = image.size
                if size!=image_size[0]*image_size[1]*3:     # this is too see that all size are enforeced to desired size
                    continue
                img_bytes = image.tostring() ### Binary conversion 
                label_bytes = label.tostring(). ## Binary Conversion 
                data = {'image' : wrap_bytes(img_bytes), 'label' : wrap_bytes(label_bytes)}
                feature = tf.train.Features(feature = data)
                example = tf.train.Example(features = feature)
                serialized = example.SerializeToString(). ## Serialization
                writer.write(serialized). # Writing to the file
            except:
                pass
     writer.close()
"""
************** To decode it back ****************
"""
### To decode the data back, you'd need a parser fucntion that decodes the binary data back to it original format and apply necessary transformations 
def parser(record):    #one record
    keys_to_features = { "image": tf.FixedLenFeature([], tf.string),"label":tf.FixedLenFeature([], tf.string)}
    parsed = tf.parse_single_example(record, keys_to_features) 
    image = tf.decode_raw(parsed["image"], tf.uint8)  # convert the binary files to int
    image = tf.cast(image, tf.float32)  # type cast it to float
    image= tf.reshape(image,(224,224,3))*(1./255)  # reshape it to the orignal image size and normalize
    label2 = tf.decode_raw(parsed["label"], tf.float64)  # convert the binary files to in
return image, label2
# This method convert the tfrecord to tf dataset
def tfdata_generator(record, is_training,batch_size):
    dataset = tf.data.TFRecordDataset(filenames=record)
    if is_training:
        dataset= dataset.apply(tf.contrib.data.shuffle_and_repeat(1024)) ## the data once reached the end on iteratiing, repeats itself
    dataset = dataset.apply(tf.contrib.data.map_and_batch(parser,batch_size))
dataset = dataset.prefetch(tf.contrib.data.AUTOTUNE)
return dataset
"""
************** To train with Tf.Data object ****************
"""
# After you create the tfrecords and have the above functions, you can initialize an iterator for these data records and input them in the keras.fit() function to train!!
training_set = tfdata_generator('/home/kshitiz/amazon_train.tfrecords', is_training=True, batch_size=_BATCH_SIZE)
testing_set  = tfdata_generator('/home/kshitiz/amazon_val.tfrecords', is_training=False, batch_size = _BATCH_SIZE)
# This is where tf iterator creates a computational graph of specified batch size and feeds into the fit(generator) method batch by batch
multi_loss_model.fit(training_set.make_one_shot_iterator(), validation_data =testing_set.make_one_shot_iterator())

Результаты и наблюдения

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

Многоуровневый классификатор

Многоотраслевой классификатор с несколькими метками

Для обоих классификаторов кривая потерь имеет тенденцию к снижению, и в результате производительность классификатора повышается. Наклон кривой потерь показывает, насколько классификатор улучшается за эпоху. Тем не менее, в обоих случаях потери по-прежнему имеют тенденцию быть еще ниже (я не мог тренироваться более 50 эпох). Для многозадачного классификатора с несколькими метками показатели для каждой ветви рассчитываются отдельно, а затем усредняются. Таким образом, графики в Multi-Branch Multi-Label Classifier показывают средние показатели за эпоху. Кривые почти всех метрик в многоветвевом классификаторе с несколькими метками более гладкие, чем в классификаторе с несколькими метками. Поскольку двоичная точность не является подходящей метрикой для задачи двоичной классификации с сильно несбалансированным набором данных, мы больше сосредоточимся на кривых точности и полноты. Многозадачные классификаторы с несколькими метками демонстрируют немного более высокую общую точность и полноту, чем классификаторы с несколькими метками, и имеют тенденцию к еще большему повышению. Наконец, результат показывает, что производительность многозадачного классификатора с несколькими метками в целом выше, чем у классификаторов с несколькими метками. Таким образом, мы можем сказать, что нейронная сеть смогла извлечь выгоду из дополнительного уровня категориальной информации, которая у нас была.

Разное

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

Я сделал пользовательскую функцию потерь для keras

## Weighted Loss--------DOES NOT WORK :'(
def weighted_loss(target, output, from_logits=False):
    if not from_logits:
    # transform back to logits
    sum_weights = tf.reduce_sum(target, axis=0)
    weight = tf.convert_to_tensor((_BATCH_SIZE - sum_weights)/_BATCH_SIZE)
    loss = -tf.reduce_sum(weight *target * tf.log(output+1e-06) + (1-weight)*(1-target)*tf.log(1-output+1e-06))
    return loss

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