![](https://habrastorage.org/getpro/habr/upload_files/e94/0b8/98f/e940b898f0ba9a3a79b4ff4cde8e6859.png)
В процессе работы над небольшим учебным проектом родилась идея системы, которой захотелось поделиться. Верхнеуровнево она до безобразия проста: на вход подаём спутниковый снимок, на выходе имеем изображение местности, в середине — обученная этому действу нейронная сеть. Забегая вперёд, работает подход (в самом минимальном приближении) примерно вот так:
![Демонстрация генерации изображений ландшафта Демонстрация генерации изображений ландшафта](https://habrastorage.org/getpro/habr/upload_files/ce3/5a2/c3a/ce35a2c3a2d36188f9c2d8cf1468a581.gif)
Таким образом задача искомого решения такая: интерпретировать спутниковый снимок (и только его) и на основе выявленных закономерностей сгенерировать фотореалистичное изображение местности (виртуальный наблюдатель стоит на земле в центре координат спутникового изображения и делает снимок того, что видит).
Общая картина
Чтобы «съесть» этого слона, делим его на две части: первая (энкодер) представляет спутниковый снимок в виде набора признаков, вторая (генератор) создает картинку, поглядев на признаки, полученные энкодером. Набор всевозможных состояний извлеченных признаков формирует так называемое скрытое пространство.
![Схема модели для генерация изображения ландшафта по спутниковому снимку Схема модели для енерация изображения ландшафта по спутниковому снимку](https://habrastorage.org/getpro/habr/upload_files/ff8/204/17d/ff820417d12c562857578b771182d549.png)
Для обучения подобной системы, какой бы она не была по своей структуре, нужен набор данных, включающий в себя связанные географическими координатами пары спутниковых снимков и фотографий местности. Примерно таких:
![Желаемый состав датасета: пары изображений, связанные геокоординатами Желаемый состав датасета: пары изображений, связанные геокоординатами](https://habrastorage.org/getpro/habr/upload_files/ba5/dd9/7f0/ba5dd97f0c73973a08f745cbf7433ce7.png)
Процесс обучения итоговой пары моделей энкодера и генератора можно разбить на два этапа:
Обучение генеративно-состязательной сети для создания фотографий из состояний скрытого пространства
Обучение сети энкодера, преобразующего признаки спутникового снимка в состояние скрытого пространства
![Схема обучения энкодера и генератора Схема обучения энкодера и генератора](https://habrastorage.org/getpro/habr/upload_files/e58/68a/4af/e5868a4afc52be120f2b3bf26dd95a25.png)
Первый этап не зависит от второго, его итогом является модель-генератор, создающая изображения, схожие представленным в датасете. На втором этапе, имея уже работающий генератор, энкодер обучается заставлять его генерировать пейзажи схожие исходным, в точках, где вторые были сняты.
Формирование датасета
Подходящего датасета фотографий ландшафта (без людей, машин и прочих посторонних предметов) с геопривязками найти не удалось. Поэтому в моем случае источниками изображений явились API VK (для изображений местности) и Яндекс Карт (для спутниковых снимков).
Изображения местности
Отбор подходящих для обучения генератора пользовательских фотографий из VK — та ещё задача. Потребовался многостадийный процесс их фильтрации, чтобы оставить ограниченный набор сцен для обучения генератора. Получить сырой набор публичных фотографий просто, вот пример такой функции:
def get_user_photos(lat, long, radius, time_range, offset=0):
params = {
'lat': lat,
'long': long,
'count': '1000',
'offset': offset,
'radius': radius,
'start_time': time_range[0],
'end_time': time_range[1],
'access_token': VK_ACCESS_TOKEN,
'v': VK_VERSION,
'sort': 0
}
return requests.get("https://api.vk.com/method/photos.search",
params=params, verify=True).json()
Широта, долгота, радиус и временной промежуток поиска. Но пользовательский контент — это быт за редким счастливым исключением. В общем виде последовательный процесс получения итогового набора изображений вышел следующим:
Получение списка публичных фотографий, загруженных пользователями
Отсев изображений, не имеющих привязки к географическим координатам
Отбор изображений с помощью сторонней сети-классификатора согласно списку подходящих классов (в моем случае всяческая природа)
Отсев изображений, содержащих фигуры и лица людей (также используя предобученную стороннюю нейросеть)
Ручной отбор неподходящих снимков, пропущенных предыдущими фильтрами
В итоге из всего загруженного объема фотографий остается всего около 0.5%, что делает создание подходящего датасета настоящим испытанием.
Спутниковые изображения
Здесь задача оказывается значительно проще. Для географических координат каждой фотографии из пункта выше получаем спутниковый снимок с помощью Static API Яндекс Карт.
def get_satellite_image(lat, long):
params = {
'll': lat + ',' + long,
'size': '450,450',
'z': 15,
'l': 'sat'
}
return requests.get("https://static-maps.yandex.ru/1.x/",
params=params, verify=True)
В итоге были получены два набора фотографий следующего вида:
![Примеры изображений датасета Примеры изображений датасета](https://habrastorage.org/getpro/habr/upload_files/dfb/574/644/dfb574644a0feb930bc55228d4e88bf3.png)
Про объем. Во-первых, чем больше, тем лучше (думаю потолка качества таким путем достигнуть сложно). Во-вторых, чем шире набор классов (горная местность, населенные пункты, дорожная инфраструктура и т.п.), тем больше примеров нужно генератору, чтобы научиться воспроизводить все эти типы ландшафта. В своем случае я оставил скромный набор из фотографий природы средней полосы в теплое время года.
Обучение модели
Архитектуры сетей GAN и энкодера привожу в довольно примитивном символическом варианте, который использовался в эксперименте. При должном усердии в формировании набора обучающих данных и свободе в вычислительных ресурсах, можно, конечно, пойти дальше и опробовать гораздо более интересные модели.
Обучение генератора
Так как набор сцен и состояний сильно ограничен, оказалось, что двузначная, а тем боле трехзначная, размерность скрытого пространства избыточна (сеть долго обучается и склонна к переобучению), поэтому пришлось остановиться на 4-мерном векторе. Далее последовательное применение 5 слоёв, использующих двумерные транспонированные свёртки размером 4 на 4 с шагом 2.
![Архитектура сверточной сети генератора Архитектура сверточной сети генератора](https://habrastorage.org/getpro/habr/upload_files/e31/8cb/27f/e318cb27ffabcd8d4ad6464828e2e0c9.png)
На выходе получаем цветное трёхканальное квадратное изображение размером 128 на 128 пикселей. В силу того, что вариативность обучающих сцен достаточно небольшая, сеть обучалась с нуля, а не использовался fine tuning предобученного генератора.
nz = 4 # размерность скрытого пространства
ngf = 64 # размерность карт признаков генератора
nc = 3 # число каналов изображения на выходе
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.main = nn.Sequential(
nn.ConvTranspose2d(nz, ngf * 16, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 16),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 16, ngf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
nn.ConvTranspose2d( ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
nn.Tanh()
)
def forward(self, input):
return self.main(input)
Дискриминатор в свою очередь является практически зеркальным отражением генератора. Поступающее на вход изображение сквозь последовательные свёрточные слои приводится к скалярной величине от 0 до 1, соответствующей вероятности того, что это изображение является сгенерированным нейросетью.
![Архитектура сверточной сети дискриминатора Архитектура сверточной сети дискриминатора](https://habrastorage.org/getpro/habr/upload_files/1d1/335/fe6/1d1335fe6e8c2706a260cff429342c9f.png)
Соответствующий дискриминатору код также почти зеркален, двумерные свёрточные слои занимают место транспонированных свёрток.
ndf = 64 # размерность карт признаков дискриминатора
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 8, ndf * 16, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 16),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 16, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, input):
return self.main(input)
В отличие от дискриминатора, который оставался достаточно точным при любом достигнутом числе итераций, генератор, по визуальной оценке, достигал максимального качества и начинал трансформировать уже, кажется, хорошо сформировавшиеся карты признаков. Поэтому шаг обучения генератора дополнительно корректировался шедулером StepLR, чтобы не промахиваться мимо локальных минимумов. При использовавшихся шагах обучения, и наборе данных, своего потолка сеть достигает примерно на 800 эпохе.
![Примеры реальных и сгенерированных изображений Примеры реальных и сгенерированных изображений](https://habrastorage.org/getpro/habr/upload_files/683/ad6/082/683ad60825dca7dd2564efae8a23da46.png)
Обучение энкодера
Как уже было сказано ранее, задача энкодера сводится к выделению на спутниковых снимках признаков, отображающихся в такие состояния скрытого пространства, из которых генератор создаст изображения, адекватные этой местности. Для спутникового снимка места, покрытого лесом, сгенерируется лесной пейзаж, для травянистой равнины — изображение луга и так далее.
Выше уже была приведена схема обучения энкодера. Поэтапно она выглядит так:
Энкодер получает на вход спутниковые снимки в координатах c1, c2 ... cn
Последовательными сверками выделяются признаки и приводятся к вектору в скрытом пространстве
Генератор (уже обученный ранее) создает на основе этого вектора новые изображения
Эти изображения сравниваются с реальными, полученными в координатах c1, c2 ... cn
Веса модели корректируются, переходим к пункту 1
ndf = 16 # размерность карт признаков энкодера
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
self.main = nn.Sequential(
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 8, ndf * 16, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 16),
nn.LeakyReLU(0.2, inplace=True),
nn.Flatten(),
nn.Linear(ndf*16*4*4, nz)
)
def forward(self, input):
return self.main(input)
Архитектура энкодера достаточно схожа с дискриминатором, имеет столько же сверточных слоев с одинаковыми параметрами. Однако для энкодера выбраны вчетверо меньшие размеры карт признаков, так как вариативность спутниковых снимков, использованных в рамках задачи достаточно небольшая.
![Архитектура сверточной сети энкодера Архитектура сверточной сети энкодера](https://habrastorage.org/getpro/habr/upload_files/364/5af/e58/3645afe5894b55dc2e8d46cfdd2d6cd5.png)
Здесь основной сложностью является выбор корректной функции потерь, позволяющей понять насколько сгенерированное изображение отвечает тому, что мы видим на реальной фотографии этой местности. Об этом далее.
Функция потерь
Нагляднее — на примере. Имеем три пары схожих по классу ландшафта изображений (условно: поле, водоем, лес), которые должны получить меньший loss при сравнении внутри пары, нежели между соседями.
![Примеры изображений для сравнения Примеры изображений для сравнения](https://habrastorage.org/getpro/habr/upload_files/979/672/e73/979672e733a67d4f49cd87d1f3c9ba71.png)
Идеальная для данной задачи функция должна вернуть минимальные значения для схожих пар. Эти значения расположились на главной диагонали матрицы на рисунке ниже. Однако популярные в таких делах MSE и SSIM не отвечают этому в достаточной степени:
![Результаты MSE и SSIM loss для попарного сравнения изображений Результаты MSE и SSIM loss для попарного сравнения изображений](https://habrastorage.org/getpro/habr/upload_files/b66/c8a/d33/b66c8ad33b4d990995467bbf14b98ad8.png)
Проблему решила специальная функция потерь, основанная на предобученной модели классификатора, использовавшегося при формировании датасета (ResNet, обученная на датасете Places365). Схема проста:
Получаем поклассовые вероятности для первого изображения
Получаем поклассовые вероятности для второго изображения
Считаем MSE loss для этих двух тензоров.
Примеры классов для реальных и сгенерированных изображений изображений выглядят следующим образом:
![Примеры классов, использовавшихся при сравнении реальных и сгенерированных изображений Примеры классов, использовавшихся при сравнении реальных и сгенерированных изображений](https://habrastorage.org/getpro/habr/upload_files/f39/66e/135/f3966e1357a0acbcf83ecbf7fb5111a2.png)
Код примененной функции потерь выглядит примерно так (где MSE_CLASS — часть, отвечающая за «правильность» содержания сгенерированного изображения, а MSE_NORM — за «нормальность» полученного распределения):
def class_norm_criterion(img_tensor1, img_tensor2, mean, variance):
logit1 = classifier.forward(img_tensor1)
logit2 = classifier.forward(img_tensor2)
MSE_CLASS = mseloss(logit1, logit2)
MSE_NORM = mean.pow(2) + (1 - variance).pow(2)
return MSE_CLASS + MSE_NORM * 0.5
Отдельной сложностью здесь явилось то, что на самых первых эпохах связка энкодер-генератор имела свойство сваливаться к краям скрытого пространства, откуда впоследствии выбраться не удавалось. Поэтому была введена поправка на степень близости полученной выборки состояний в пределах батча к нормальному распределению (по аналогии с расстоянием Кульбака — Лейблера). Эта мера адекватна при достаточно больших размеров батчей, так как при обучении генератора использовался шум с нормальным распределением.
![Результаты работы функции потерь на основе классификатора Результаты работы функции потерь на основе классификатора](https://habrastorage.org/getpro/habr/upload_files/259/b27/f64/259b27f6402ed9586ea04bba601282f8.png)
Итоговые результаты превзошли по точности MSE и SSIM. Энкодер с такой комплексной функцией потерь обучается медленнее, но качественно лучше.
Результат работы генератора
По результатам работы связка энкодер-генератор таки научилась создавать изображения, минимально адекватно описывающие то, что открывается взору наблюдателя в местности, запечатленной спутником.
![Результат работы обученной модели: спутниковые снимки, реальные и сгенерированные фотографии местности Результат работы обученной модели: спутниковые снимки, реальные и сгенерированные фотографии местности](https://habrastorage.org/getpro/habr/upload_files/52c/b8a/f6f/52cb8af6f4d1ec599f7ed3ff17fa0bb5.png)
реальные и сгенерированные фотографии местности
При большом желании, с кодом, использовавшимся для создания всего этого безобразия, можно ознакомиться здесь.
Где применять
В голову приходит ряд примеров, когда требуется визуальное подкрепление, а единственной актуальной и достоверной информацией о местности служит спутниковый снимок.
Валидация загружаемых пользователями фотографий
Зачастую, в картографических сервисах (живой пример - Яндекс Карты) попадаются объекты, например фотографии или панорамы, с ошибочной геопривязкой. Проверить корректность загружаемой фотографии заявленной точке на карте - подходящая задача для такой модели.
![Валидация пользовательского контента на геосервисе Валидация](https://habrastorage.org/getpro/habr/upload_files/8e8/022/a4e/8e8022a4e770b105ef6908a8df260c70.png)
Визуальная фильтрация по типу местности
Пример - фильтр для выбора максимально подходящей местности на карте для сервиса подбора недвижимости.
![Фильтрация по типу местности Фильтрация](https://habrastorage.org/getpro/habr/upload_files/1e7/816/377/1e78163773f8bd91a98c8a3f90133257.png)
Генерация контента
Для природных (а в общем смысле подхода и не только) объектов на карте, не снабженных пользовательскими фотографиями.
![Генерация контента для геосервисов Контент](https://habrastorage.org/getpro/habr/upload_files/4d4/51c/254/4d451c254bd7b1edbda5e082661e71ba.png)
Вместо заключения
Думаю, можно с уверенностью утверждать, что при кратно большем (как по объёму так и разнообразию) наборе обучающих данных и доступных вычислительных мощностях результаты генерации должны значительно превзойти полученные в работе. Как в плане размера изображений и их качества, так и в плане количества типов ландшафтов (населенные пункты, горная местность, дорожная инфраструктура и т.п.). Совсем замечательно было бы физически собрать свой датасет, с учетом ориентации поля зрения камеры. Но это уже совсем другая история.