Please use this identifier to cite or link to this item:
https://er.chdtu.edu.ua/handle/ChSTU/6733| Title: | Прогнозування врожайності сільськогосподарських культур із використанням методів глибинного навчання |
| Authors: | Карапетян, Анаіт Радіківна Козак, Назар Ігорович |
| Keywords: | МУЛЬТИМОДАЛЬНІ ДАНІ, СІЛЬСЬКЕ ГОСПОДАРСТВО, КОНТРАСТИВНЕ НАВЧАННЯ, ПРИЧИННА ЧУТЛИВІСТЬ, ПРОГНОЗУВАННЯ NDVI, СЦЕНАРНИЙ АНАЛІЗ;MULTIMODAL DATA, AGRICULTURE, CONTRASTIVE LEARNING, CAUSAL SENSITIVITY, NDVI FORECASTING, SCENARIO ANALYSIS |
| Issue Date: | 18-Dec-2025 |
| Abstract: | Кваліфікаційна робота магістра присвячена прогнозуванню врожайності сільськогосподарських культур на основі аналізу мультимодальних даних із застосуванням методів глибинного навчання. Об’єктом дослідження є процеси обробки супутникових знімків, метеорологічних та ґрунтових даних, а предметом — алгоритми контрастивного самонавчання та механізми причинної чутливості для формування спільного простору представлень і прогнозування агропоказників. Метою роботи є розроблення та експериментальне дослідження методу MTCR-CS для прогнозування індексу NDVI та оцінювання впливу кліматичних факторів на стан посівів.
У роботі використано методи попередньої обробки та аугментації даних, алгоритми контрастивного навчання і глибокі нейронні мережі з оптимізацією гіперпараметрів та сценарним аналізом. Отримані результати мають практичне значення для підвищення точності прогнозування врожайності й ризиків, підтримки прийняття рішень у сільському господарстві, а також для створення веб- та GIS-систем моніторингу і подальшого поширення підходу на інші задачі з мультимодальними даними. The master’s thesis is devoted to crop yield prediction based on the analysis of multimodal data using deep learning methods. The object of the research is the processing of satellite imagery, meteorological, and soil data, while the subject comprises contrastive self-learning algorithms and causal sensitivity mechanisms for building a unified representation space and forecasting agricultural indicators. The aim of the study is to develop and experimentally evaluate the MTCR-CS method for predicting the NDVI index and assessing the impact of climate factors on crop conditions. The work employs data preprocessing and augmentation techniques, contrastive learning algorithms, and deep neural networks with hyperparameter optimization and scenario-based perturbation analysis. The obtained results are of practical importance for improving the accuracy of yield and risk forecasting, supporting decision-making in agriculture, and developing web- and GIS-based monitoring systems, as well as for extending the proposed approach to other multimodal data domains. |
| URI: | https://er.chdtu.edu.ua/handle/ChSTU/6733 |
| Appears in Collections: | 112 Статистика (Аналіз даних (DATA SCIENCE) та комп'ютерна статистика) |
Files in This Item:
| File | Description | Size | Format | |
|---|---|---|---|---|
| КОЗАК Н.І. Кваліфікаційна робота магістра.pdf Restricted Access | 3.33 MB | Adobe PDF | View/Open Request a copy |
Items in DSpace are protected by copyright, with all rights reserved, unless otherwise indicated.
Extracted text
2
РЕФЕРАТ
Магістерська кваліфікаційна робота складається зі вступу, чотирьох розділів,
висновків, списку використаних джерел та додатків. Обсяг роботи становить 63
сторінки основного тексту, містить 14 рисунків, 5 таблиці, 23 найменувань у списку
використаних джерел і 2 додатків.
Об’єктом дослідження є процеси аналізу мультимодальних
сільськогосподарських даних з використанням методів машинного навчання.
Предметом дослідження є алгоритми контрастивного самонавчання та
механізми причинної чутливості для формування єдиного простору представлень і
прогнозування агропоказників.
Метою роботи є розробка та експериментальне дослідження методу MTCR-CS
для аналізу супутникових знімків, метеоданих і ґрунтових характеристик з метою
прогнозування NDVI та оцінки впливу кліматичних змін.
Методи розробки базувалися на основних положеннях теорії машинного
навчання та глибоких нейронних мереж. Попередня обробка даних проводилася з
використанням методів агрегації, нормалізації та аугментації. Для підвищення якості
представлень застосовувалися алгоритми контрастивного навчання, а для
прогнозування — нейронні мережі. Вдосконалення моделі здійснювалося через підбір
гіперпараметрів та сценарний аналіз пертурбацій.
Практичне значення роботи спрямоване на підвищення точності прогнозування
ризиків, оптимізацію управління посівами для фермерів та агрохолдингів, створення
веб- і GIS-застосунків для інтерактивного моніторингу, а також розширення методу на
інші домени з мультимодальними даними для проактивного прийняття рішень.
Ключові слова: мультимодальні дані, сільське господарство, контрастивне
навчання, причинна чутливість, прогнозування ndvi, сценарний аналіз.
3
ABSTRACT
The Master's qualification paper consists of an introduction, four chapters,
conclusions, a list of references, and appendices.The volume of the work is 63 pages of main
text; it contains 14 figures, 5 tables, 23 items in the list of references, and 2 appendices.
The object of the research is the processes of analyzing multimodal agricultural data
using machine learning methods.
The subject of the research is contrastive self-supervised learning algorithms and
causal sensitivity mechanisms for forming a unified representation space and forecasting
agricultural indicators.
The aim of the work is the development and experimental study of the MTCR-CS
method for analyzing satellite imagery, meteorological data and soil characteristics in order
to forecast NDVI and assess the impact of climate changes.
The development methods were based on the fundamental principles of machine
learning theory and deep neural networks. Data preprocessing was performed using
aggregation, normalization and augmentation techniques. To improve the quality of
representations, contrastive learning algorithms were applied, and neural networks were
used for forecasting. Model improvement was carried out through hyperparameter tuning
and scenario-based perturbation analysis.
The practical significance of the work is aimed at increasing the accuracy of risk
forecasting, optimizing crop management for farmers and agricultural holdings, creating
web- and GIS-applications for interactive monitoring, as well as extending the method to
other domains with multimodal data to support proactive decision-making.
Keywords: multimodal data, agriculture, contrastive learning, causal sensitivity, ndvi
forecasting, scenario analysis.
4
Зміст
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ .......... 6
ВСТУП ................................................................................................................................... 8
1 ЗАГАЛЬНА ХАРАКТЕРИСТИКА РОБОТИ ................................................................ 10
1.1 Актуальність теми ............................................................................................... 10
1.2 Мета та завдання дослідження .......................................................................... 12
1.3 Аналіз сучасних підходів та огляд літератури ................................................. 14
1.3.1 Статистичні та ML-моделі на часових рядах ........................................... 15
1.3.2 Глибокі моделі на супутникових знімках ................................................. 16
1.3.3 Мультимодальні підходи та проблема інтеграції ..................................... 17
1.3.4 Сценарний аналіз у сільському господарстві ........................................... 19
1.4. Запропоноване рішення ..................................................................................... 21
1.5 Висновки до Розділу 1 ........................................................................................ 22
2 ТЕОРЕТИЧНІ ОСНОВИ ТА РОЗРОБКА МЕТОДУ MTCR-CS ................................ 25
2.1 Математична постановка задачі ........................................................................ 25
2.2 Формалізація методу MTCR .............................................................................. 26
2.3 Формалізація прогнозуючої функції ................................................................. 28
2.4 Формалізація сценарного аналізу ...................................................................... 29
2.5 Висновки до розділу 2 ........................................................................................ 31
3 ПРАКТИЧНА РЕАЛІЗАЦІЯ МЕТОДУ MTCR-CS ...................................................... 34
3.1 Підготовка даних ................................................................................................. 34
3.1.1 Підготовка картографічних даних .............................................................. 35
5
3.1.2 Підготовка погодних даних ......................................................................... 40
3.1.3 Фінальна структура даних ........................................................................... 43
3.2 Реалізація методу MTCR .................................................................................... 44
3.3 Вибір архітектури та навчання прогнозуючої функції .................................... 49
3.4 Сценарний аналіз ................................................................................................ 52
3.5 Об’єднане рішення .............................................................................................. 55
3.6 Висновки до розділу 3 ........................................................................................ 56
4 АНАЛІЗ РЕЗУЛЬТАТІВ ДОСЛІДЖЕННЯ ТА ПЕРСПЕКТИВИ ЇХ
ВИКОРИСТАННЯ ....................................................................................................... 58
4.1 Аналіз отриманих результатів ........................................................................ 58
4.2 Практична цінність ......................................................................................... 60
4.3 Обмеження запропонованого підходу ........................................................... 63
4.4 Перспективи подальших досліджень ............................................................ 65
4.5 Висновки до 4 розділу .................................................................................... 66
ВИСНОВКИ ........................................................................................................................ 68
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ .......................................................................... 70
ДОДАТОК А.Специфікація ............................................................................................... 73
ДОДАТОК Б. Текст програми ........................................................................................... 76
6
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ
ARIMA – Autoregressive Integrated Moving Average. модель для прогнозування
часових рядів
BYOL – Bootstrap Your Own Latent, метод самонавчання в глибокому навчанні
CNN – Convolutional Neural Network, згорткова нейронна мережа
CS – Causal Sensitivity, механізм причинної чутливості
FAO – Food and Agriculture Organization, Організація Об'єднаних Націй з питань
продовольства та сільського господарства;
InfoNCE – Information Noise-Contrastive Estimation, функція втрат для
контрастивного навчання;
IoU – Intersection over Union, метрика для оцінки сегментації;
LAI – Leaf Area Index. індекс площі листя;
LSTM – Long Short-Term Memory, рекурентна мережа для часових рядів
MAE – Mean Absolute Error, середня абсолютна похибка;
MLP – Multi-Layer Perceptron, багатошаровий перцептрон;
MoCo – Momentum Contrast, метод контрастивного навчання з динамічним
словником;
MODIS – Moderate Resolution Imaging Spectroradiometer, супутниковий датчик
NASA;
MSE – Mean Squared Error, середньоквадратична похибка;
MTCR – Multimodal Temporal Contrastive Representation, мультимодальне часове
контрастивне представлення;
MTCR-CS – Multimodal Temporal Contrastive Representation with Causal Sensitivity,
розширення MTCR з причинною чутливістю;
NDVI – Normalized Difference Vegetation Index, нормалізований різницевий індекс
вегетації;
7
PLS - Partial Least Squares, часткові найменші квадрати; регресійний метод для
часових рядів.
RMSE – Root Mean Squared Error, корінь середньоквадратичної похибки;
RNN – Recurrent Neural Network, рекурентна нейронна мережа;
SAR – Synthetic Aperture Radar, синтетична апертурна радіолокація; радарні дані для
супутникових знімків.
SimCLR – Simple Framework for Contrastive Learning of Visual Representations,
простий фреймворк контрастивного навчання;
8
ВСТУП
Сучасний розвиток сільського господарства відбувається в умовах стрімкого
зростання обсягів даних, що надходять із різноманітних джерел — супутникового
дистанційного зондування, метеорологічних станцій, наземних сенсорів та
інформаційних систем агровиробників. Ці дані мають принципово різну природу:
просторово-розподілені растрові зображення, часові ряди високої частоти, табличні та
векторні геодані. Така гетерогенність відкриває нові можливості для точного
землеробства, проте водночас створює серйозні виклики для їх ефективної обробки та
інтеграції.
Традиційні статистичні методи та моделі машинного навчання з учителем, які
домінували в агроаналітиці до недавнього часу, виявляються недостатніми для роботи
з мультимодальними даними великого обсягу. Вони вимагають значної кількості
ручної розмітки, погано узагальнюються при зміні регіону чи культури та не
дозволяють оперативно оцінювати гіпотетичні сценарії («що буде, якщо температура
зросте на 3 °C» або «опади зменшаться на 30 %»). У той же час саме такі сценарні
оцінки є ключовими для переходу від реактивного до проактивного управління
кліматичними та виробничими ризиками.
Останні досягнення в галузі глибокого навчання, зокрема поява самонавчальних
і контрастивних підходів, дали потужний інструментарій для формування
інваріантних представлень без великої кількості міток. Проте більшість сучасних
контрастивних архітектур (SimCLR, MoCo, BYOL та їх похідні) розроблені для
однорідних даних — зображень або тексту — і не враховують специфіку агро-задач:
сильну сезонну циклічність, просторово-часову гетерогенність, необхідність
інтерпретованих висновків на рівні окремого поля.
Таким чином, існує науково-практична прогалина між наявними
універсальними мультимодальними моделями та реальними потребами аграрного
9
сектору України, який працює в умовах високої кліматичної нестабільності та
потребує інструментів оперативного прогнозування й оцінки ризиків.
Метою кваліфікаційної роботи є розробка, реалізація та експериментальне
дослідження мультимодального часового контрастивного підходу з механізмом
причинної чутливості (MTCR-CS), який забезпечує формування єдиного простору
представлень гетерогенних сільськогосподарських даних, прогнозування стану
посівів та інтерпретований сценарний аналіз кліматичних ризиків без перенавчання
моделі.
10
1 ЗАГАЛЬНА ХАРАКТЕРИСТИКА РОБОТИ
1.1 Актуальність теми
Сільське господарство є стратегічною галуззю економіки України, що формує
значну частку валового внутрішнього продукту та експорту країни. За даними
Міністерства аграрної політики та продовольства України, в 2023 році аграрний сектор
становив близько 12 % ВВП, а обсяг експорту сільськогосподарської продукції
перевищив 45 млрд доларів США [1]. Водночас сектор стикається з низкою системних
викликів — змінами клімату, дефіцитом водних і земельних ресурсів, а також
потребою в підвищенні продуктивності через точні технології землеробства.
Зростання світового населення й відповідне підвищення попиту на продовольство (за
прогнозами FAO, глобальний попит може зрости на ~70 % до 2050 року) роблять
Україну важливим гравцем у міжнародних ланцюгах постачання продовольства [2]. У
цьому контексті ефективний аналіз аграрних даних набуває стратегічного значення
для підвищення стійкості й продуктивності виробництва.
Сучасна аграрна аналітика спирається на великі й гетерогенні масиви даних, у
тому числі супутникові знімки (оптичні та радарні), наземні сенсорні вимірювання,
метеорологічні спостереження та статистичні табличні дані. Розвиток програмних і
апаратних платформ дистанційного зондування (наприклад, місії Copernicus —
Sentinel-1/2 та супутникові місії сімейства Landsat) забезпечив щоденне надходження
об’ємних потоків даних різної природи: оптичних зображень, радарних (SAR) даних і
часових рядів спектральних індексів. Ілюстративно, великі публічні набори даних,
такі як SEN12MS та BigEarthNet, демонструють масштаб доступних мультимодальних
наборів (див. [3], [4]), що відкриває широкі можливості для тренування і тестування
моделей машинного навчання в задачах моніторингу земельного покриву та оцінки
стану сільськогосподарських угідь.
11
Разом із тим збільшення обсягів і різноманітності даних породжує низку
методологічних проблем при їх обробці й інтеграції. Нижче виділено три ключові
класи проблем, характерних для мультимодального агродатасету:
1. Проблема представлення. Супутникові зображення характеризуються великою
просторовою роздільністю й високою вимірністю (тисячі—мільйони пікселів), тоді як
табличні дані (метео-, ґрунтові показники) зазвичай мають низьку розмірність і іншу
семантику ознак. Це ускладнює формування компромісного представлення, яке
одночасно зберігає інформацію про просторові закономірності та числові метео-
/ґрунтові характеристики.
2. Проблема вирівнювання за часом і простором. Часові ряди індексів вегетації
(наприклад, NDVI, LAI) можуть мати пропуски або нерівномірну розрядність через
хмарність і періодичність спостережень, тоді як метеорологічні дані надходять з
регулярною кроковістю. Узгодження часових і просторових масштабів при цьому
часто призводить до втрати короткочасних екстремумів або до неявної агрегації
важливих сигналів (наприклад, нічні заморозки), що зменшує чутливість моделей до
критичних подій.
3. Проблема інтеграції (data fusion). Застосування ранніх підходів злиття даних
(early fusion) може викликати «прокляття розмірності» через різке збільшення числа
ознак, тоді як пізні підходи (late fusion) іноді втрачають міжмодальні кореляції,
важливі для точного прогнозування. Середні підходи (mid-level fusion), які
передбачають попередню екстракцію і зменшення розмірності, показують найкращий
компроміс у низці досліджень [5], проте питання узагальненості таких рішень ще
відкрито.
Практичні дослідження підтверджують, що комбінування мультиспектральних
супутникових даних із наземними сенсорами може суттєво покращувати результати
прогнозування врожайності та виявлення агро-стресів. Наприклад, при інтеграції
даних Sentinel-2 і вимірів вологості ґрунту з IoT-сенсорів показники пояснювальної
здатності моделей (R²) були вищими у випадку застосування середньорівневого злиття
12
(типово R² ≈ 0,66–0,88 у наведених експериментах) [6]. Водночас без адекватної
попередньої обробки і вирівнювання різнорідних даних можливе зниження точності
діагностики і класифікації до 22–34 % в залежності від задачі та метрики [7]. Такі
результати демонструють як потенціал мультимодальних підходів, так і важливість
продуманих архітектур представлення даних.
Незважаючи на наявні методи злиття й представлення, загальною проблемою
залишається брак єдиного, узгодженого простору представлень, який одночасно
враховував би:
- часову динаміку системи (послідовність подій і їх причинно-наслідкові
взаємозв’язки);
- просторову гетерогенність полів і мікрокліматичних умов;
- можливість проведення сценарного аналізу типу «що буде, якщо зміняться опади
або температура».
Відсутність такого інтегрованого простору ускладнює перехід від реактивного
до проактивного управління ризиками в агросекторі. Розробка методів, які здатні
одночасно долати хмарність, просторову гетерогенність та забезпечувати сценарний
аналіз на рівні окремого поля, є нагальною потребою для підвищення стійкості та
прибутковості сільського господарства України в умовах кліматичної невизначеності.
1.2 Мета та завдання дослідження
Мета даної кваліфікаційної роботи - розробити, реалізувати та
експериментально дослідити мультимодальний часовий контрастивний підхід з
механізмом причинної чутливості (MTCR-CS) для аналізу сільськогосподарських
даних, що забезпечує:
1. формування єдиного простору представлень мультимодальних даних
(супутникові знімки, метеодані, ґрунтові характеристики, врожайність);
13
2. прогнозування NDVI та врожайності на наступний місяць на рівні
окремого поля;
3. оцінку впливу гіпотетичних змін кліматичних факторів («а що, якби?») без
перенавчання моделі.
Дослідження проводиться на прикладі 66 громад Черкаської області (2021–2023
рр.), що включає ≈198 000 полів.
Для досягнення поставленої мети необхідно вирішити такі завдання:
1. Сформувати мультимодальний набір даних:
- зібрати геометрії полів, місячні медіанні знімки Sentinel-2 (L2A), щоденні
метеодані по центроїдам громад, дані про тип ґрунту та врожайність;
- виконати агрегацію погодних даних до місячного рівня (сума опадів,
середня/максимальна/мінімільна температура, вологість);
- провести попередню обробку (видалення хмар, нормалізація, аугментація).
2. Розробити архітектуру MTCR (Multimodal Temporal Contrastive Representation):
- реалізувати окремі енкодери для кожної модальності (CNN для зображень,
MLP для табличних даних, RNN/Transformer для часових рядів);
- впровадити контрастивну функцію втрат (InfoNCE) з тимчасовими та
просторовими аугментаціями;
- сформувати єдиний векторний простір представлень розмірністю 256–512.
3. Створити прогнозуючу голову (Forecasting Head):
- реалізувати рекурентну або трансформерну архітектуру на основі MTCR-
представлень;
- навчити модель передбачати NDVI та врожайність на наступний місяць з
урахуванням припущеної погоди;
- оцінити точність за метриками MSE, MAE, R².
4. Розробити механізм причинної чутливості (Causal Sensitivity, CS):
- реалізувати пертурбаційний підхід: зміна одного або кількох кліматичних
параметрів (Δопади, Δтемпература) → перерахунок прогнозу;
14
- впровадити градієнтний аналіз чутливості (∂NDVI/∂w);
- забезпечити інтерпретованість результатів у вигляді sensitivity maps та
числових оцінок впливу.
5. Провести експериментальне дослідження:
- розбити дані на тренувальну (2021–2022), валідаційну та тестову (2023)
вибірки;
- порівняти MTCR-CS з базовими моделями: LSTM, U-Net, моделі без CS;
- оцінити стійкість до хмарності, просторової гетерогенності та шумів.
6. Оцінити практичну цінність підходу:
- продемонструвати приклади сценарного аналізу для типових ризиків (посуха,
заморозки);
- розрахувати потенційне зниження економічних втрат (грн/га) при
впровадженні;
- запропонувати рекомендації для інтеграції в системи точного землеробства.
Сформульована мета та перелік завдань чітко відповідають виявленим у
підпункті 1.1 прогалинам сучасних підходів до аналізу мультимодальних агроданих.
Виконання поставлених завдань дозволить створити науково обґрунтований,
масштабований та практично корисний інструмент для проактивного управління
ризиками в українському сільському господарстві.
1.3 Аналіз сучасних підходів та огляд літератури
Виявлені у підпункті 1.1 методологічні прогалини — зокрема проблеми
представлення, вирівнювання та інтеграції даних — безпосередньо впливають на
якість прогнозування NDVI та врожайності, що є основними цілями, визначеними у
підпункті 1.2. Аналіз сучасних досліджень у сфері аграрної аналітики свідчить про те,
що більшість існуючих підходів тяжіють до трьох основних напрямів:
15
1. статистичні та класичні методи машинного навчання, які працюють із часовими
рядами;
2. моделі глибокого навчання, орієнтовані переважно на супутникові знімки;
3. мультимодальні підходи, що поєднують дані різної природи.
1.3.1 Статистичні та ML-моделі на часових рядах
Найпоширенішим підходом до прогнозування NDVI та врожайності
залишаються моделі, засновані на часових рядах метеорологічних та вегетаційних
показників. Традиційно для цих задач використовуються регресійні моделі (лінійна,
логістична, PLS-регресія), а також ARIMA-подібні методи [8]. Такі підходи
демонструють прийнятну точність на невеликих наборах із однорідними часовими
рядами, проте вони мають обмежену здатність до узагальнення при збільшенні
кількості просторових об’єктів або при роботі з даними різної природи
(супутниковими, погодними, ґрунтовими).
У дослідженні Bolton et al. (2020) на базі даних MODIS для штату Айова, США
модель ARIMA досягла RMSE = 0,072 для прогнозу NDVI, використовуючи лише
метеорологічні змінні (температура, опади) та історичні значення NDVI [9]. Проте
така модель ігнорувала просторові особливості: аналізувалося лише одне середнє
значення NDVI на піксель 250 м² та не враховувала текстурну інформацію з
супутникових зображень, що призвело до систематичних помилок у мікрокліматичних
зонах і недооцінки впливу локальних погодних умов.
Більш складні моделі машинного навчання, такі як Random Forest та XGBoost,
дозволяють інтегрувати додаткові табличні ознаки такі як ґрунтові характеристики,
сівозміну та агротехнічні параметри. У роботі Cai et al. (2019) [10] XGBoost із раннім
об’єднанням ознак - конкатенацією 12 метеорологічних, 8 ґрунтових показників та
NDVI - досяг R² = 0,74 для прогнозу врожайності рису на 1200 полях Китаю. Однак
просте об’єднання понад 20 ознак без належної нормалізації призвело до домінування
16
метеоданих. Їхній внесок у важливість ознак перевищив 80 %, тоді як внесок
супутникових зображень Sentinel-2, агрегованих як середнє значення по полю, склав
менше ніж 3 %. Автори відзначають прояви перевчання на шумі та втрату просторових
патернів, зокрема, неможливість виявлення локальних посух усередині полів.
Дослідження останніх років [9], [10] узгоджено свідчать, що класичні та
ансамблеві методи машинного навчання недостатньо враховують нелінійні залежності
між погодними факторами, ґрунтовими властивостями та спектральними показниками
вегетації. Крім того, такі моделі не мають механізмів узгодження часової динаміки між
модальностями (наприклад, між щоденними метеоспостереженнями та місячними
супутниковими знімками), що обмежує їх застосування в мультимодальних сценаріях
прогнозування.
1.3.2 Глибокі моделі на супутникових знімках
З появою відкритих супутникових даних Sentinel-1/2, Landsat-8/9 та великих
наборів даних, таких як SEN12MS і BigEarthNet, різко зросла кількість досліджень,
присвячених застосуванню глибоких нейронних мереж для аналізу стану посівів,
класифікації культур і прогнозування вегетаційних показників [11]–[13].
Найпоширенішими архітектурами залишаються Convolutional Neural Networks
(CNN) та U-Net, які дозволяють моделі враховувати просторові закономірності на рівні
пікселів. Наприклад, у роботі Weiss et al. (2020) модель U-Net, навчена на стеках
Sentinel-2 (12 спектральних каналів × 6 місяців), досягла IoU = 0,91 при сегментації
полів, проте прогнозування врожайності здійснювалось лише через регресію на
останньому знімку, що дало R² = 0,68 [12]. Це демонструє головне обмеження таких
підходів - ігнорування часової динаміки, тобто відсутність урахування змін NDVI
упродовж сезону.
З розвитком часових архітектур (LSTM, GRU, ConvLSTM, Vision Transformer)
з’явилися спроби поєднати супутникові серії у просторово-часові моделі. У
17
дослідженні Wang et al. (2021) було запропоновано підхід early fusion: метеодані
(температура, опади, вологість, вітер) додавалися як додаткові канали до спектральних
зображень Sentinel-2, формуючи 16-канальний тензор. Модель ConvLSTM досягла R²
= 0,71 для прогнозу NDVI [13]. Однак через різну природу даних — піксельні значення
проти скалярних показників — CNN фактично “розмивала” метеосигнал по всьому
зображенню. У результаті спостерігалося завищення NDVI у посушливих ділянках (≈
+0,11) та заниження у вологих (≈ −0,09). Самі автори визнають, що «метеодані
втрачають локальність, стаючи шумом».
Таким чином, хоча CNN, U-Net і трансформерні архітектури забезпечують
високу точність у межах однієї модальності (оптичні зображення), вони не здатні
ефективно інтегрувати гетерогенні дані (метео, ґрунт, супутникові) та узгодити часову
динаміку між джерелами. Це обмежує їх застосування для повноцінного
мультимодального аналізу, що є підґрунтям для пошуку нових методів — таких як
запропонований у цій роботі Multimodal Temporal Contrastive Representation (MTCR).
1.3.3 Мультимодальні підходи та проблема інтеграції
Мультимодальні моделі у сільськогосподарській аналітиці намагаються
інтегрувати дані різної природи - супутникові зображення, метеорологічні часові ряди,
ґрунтові та статистичні показники. Метою таких підходів є побудова єдиного
представлення стану агроекосистеми, яке одночасно враховує просторові, часові та
екологічні фактори. Проте ключовою проблемою залишається спосіб злиття даних, від
якого безпосередньо залежить якість узагальнення та інтерпретованість моделі.
Основні підходи до інтеграції даних поділяють на три типи — early fusion, mid-level
fusion та late fusion [14].
Early fusion передбачає пряме об’єднання ознак із різних джерел перед подачею
у спільну модель. Це дозволяє зберегти всю інформацію, але часто призводить до
18
«прокляття розмірності» та втрати статистичної стійкості. У роботі Khaki et al. (2020)
для прогнозування врожайності кукурудзи CNN та MLP об’єднують:
• 12-канальні знімки Sentinel-2
• 7 метеопоказників за 30 днів
• 5 ґрунтових характеристик
Загалом формується 227 ознак, що подаються в багатошаровий перцептрон.
Модель досягає R² = 0,76, але демонструє надмірну залежність від метеопараметрів,
тоді як внесок супутникових зображень становить менше 5 %. Додавання SAR-даних,
тобто зображень землі отримані за допомогою радіохвиль - лише погіршило результат
(R² = 0,71) через посилення шуму та мультиколінеарність [14]. Таким чином, раннє
злиття не лише не підвищує точність, а й ускладнює навчання через некоректне
масштабування та різну природу ознак.
Late fusion, навпаки, передбачає побудову окремих моделей для кожної
модальності з подальшим поєднанням їхніх прогнозів. Такий підхід частково знижує
ризик різкого збільшення кількості ознак, але втрачає міжмодальні взаємозв’язки.
Наприклад, You et al. (2021) застосували LSTM для часових рядів NDVI та Random
Forest для метеоданих, об’єднуючи їхні прогнози через усереднення. Отримано R² =
0,79, але при хмарності більше 60-ти % модель LSTM повертала пропуски (NaN), тоді
як RF не враховував просторові патерни (локальні посухи, хвороби рослин). У
результаті середня похибка досягала ±1,2 т/га для кукурудзи на полях з частковим
водним дефіцитом [15].
Найбільш збалансованим вважається mid-level fusion, де кожна модальність
проходить окреме кодування у векторні представлення (латентні простори), які потім
агрегуються. У роботі Zhang et al. (2023) використано CNN-енкодер для Sentinel-2 та
Transformer для погодних рядів. Після формування двох 512-вимірних векторів
проводиться конкатенація та подальше прогнозування врожайності. Модель показала
R² = 0,84, що значно краще за early та late fusion. Однак її обмеження полягає у
відсутності самонавчального або контрастивного етапу - модель використовує підхід
19
навчання з учителем, що вимагає понад 10 000 міток врожайності для стабільного
результату. [16].
Отже, попри помітний прогрес у напрямі мультимодального моделювання,
більшість існуючих рішень залишаються кореляційними, тобто вони фіксують
співзалежності між ознаками, але не здатні виявити причинно-наслідкові зв’язки між
погодними умовами та змінами вегетаційних індексів. Саме ця обмеженість
підкреслює необхідність подальшого розвитку підходів, які поєднують
мультимодальність із причинним аналізом, що розглядається у наступному підпункті.
1.3.4 Сценарний аналіз у сільському господарстві
Сценарний або причинний аналіз («а що, якби?») є важливою складовою
сучасних систем підтримки прийняття рішень у сільському господарстві, оскільки
дозволяє оцінювати наслідки гіпотетичних змін погодних умов, стресових факторів чи
управлінських дій без проведення реальних експериментів. У класичних підходах
сценарний аналіз базується на статистичних та економетричних моделях, однак їх
застосування до мультимодальних супутникових даних є нетривіальним через високу
просторова-гетерогенність, нелінійні взаємодії та складні часові залежності. Нижче
розглянуто основні групи методів, що формують сучасний стан цієї галузі.
Одним з найбільш широко застосовуваних інструментів є статистичні моделі
втручань, зокрема метод CausalImpact, який використовує Bayesian structural time-
series (BSTS) для оцінки відхилення спостережуваного ряду від контрфактичного
прогнозу. У класичній роботі Brodersen et al. (2015) метод використано для аналізу
впливу рекламних кампаній, проте він також адаптований до аграрних задач, зокрема
для оцінки впливу посухи на базі історичних рядів NDVI та метеоданих [17].
Аналогічно, дослідження Van Klompenburg et al. (2021) демонструє, що зменшення
кількості опадів на 30 % у Нідерландах призводить до зниження NDVI на 0,15 ± 0,04
з імовірністю 92 % [18]. Втім, такі моделі працюють виключно з одновимірними
20
часовими рядами, ігнорують просторову структуру полів і не дозволяють інтегрувати
супутникові зображення або інші типи даних. Крім того, вони потребують чіткого
моменту «втручання», який у реальних кліматичних сценаріях зазвичай відсутній.
Другим напрямом є графові методи причинності, що реалізують фреймворк
Pearl через побудову спрямованих ациклічних графів (DAG). Бібліотека DoWhy надає
інструменти для ідентифікації причинно-наслідкових ефектів та оцінки average
treatment effect (ATE) [19]. Наприклад, роботи Sharma et al. (2021) та Li et al. (2022)
демонструють можливість застосування DAG для оцінки впливу іригації або змін
опадів на врожайність через посередницькі змінні, як-от NDVI [19, 20]. Проте
побудова коректного DAG у сільському господарстві є складним завданням, що
вимагає значних експертних знань щодо взаємодії погодних, ґрунтових та біофізичних
процесів. Крім того, такі методи не призначені для аналізу тензорних даних
супутникових зображень і не масштабуються на великі набори полів.
Гібридні ML-причинні підходи, такі як Double/Debiased Machine Learning,
спрямовані на оцінку причинних ефектів за використання високопродуктивних ML-
моделей як «інструментів» для контролю конфундерів. Роботи Chernozhukov et al.
(2018) та Schluter et al. (2023) демонструють, що ці методи здатні оцінювати
гетерогенні ефекти температурних аномалій на врожайність у різних регіонах [21, 22].
Однак дані підходи працюють переважно з табличними даними та вимагають великої
кількості міток, що робить їх малопридатними для мультимодальних супутникових
наборів, де кількість полів значно перевищує кількість доступних виміряних врожаїв.
Окрему групу становлять пертурбаційні методи у глибокому навчанні, де
сценарії моделюються шляхом цілеспрямованих змін у вхідних даних нейронної
мережі. Наприклад, у роботі Russakovsky et al. (2022) досліджується вплив штучного
підвищення температури на прогнозовану врожайність шляхом зміни відповідних
каналів у вході CNN [23]. Такий підхід дозволяє отримати швидкі оцінки чутливості
моделі, проте не враховує складних зворотних зв’язків між модальностями (зміна
21
температури впливає на NDVI, а NDVI — на інші змінні), що робить отримані сценарії
потенційно нереалістичними.
Узагальнюючи попереднє, сучасні підходи до причинного аналізу в аграрному
секторі значною мірою орієнтовані на табличні дані та одновимірні часові ряди, тоді
як мультимодальні супутникові дані залишаються практично невикористаними у
цьому контексті. Відсутність моделей, здатних формувати спільний латентний простір
для зображень, метеорологічних рядів та ґрунтових характеристик, суттєво ускладнює
реалізацію узгодж еного та інтерпретованого сценарного аналізу на рівні окремих
полів.
Подальші дослідження зосереджуються на побудові адаптивних
мультимодальних архітектур, що враховують часову динаміку, причинність та
інтерпретованість. Одним із таких рішень є розроблений у цій роботі підхід MTCR-
CS (Multimodal Temporal Contrastive Representation with Causal Sensitivity), який
поєднує контрастивне навчання для формування узгодженого представлення даних та
механізм причинної чутливості для проведення сценарного аналізу без необхідності у
графах причинності чи великій кількості міток.
1.4. Запропоноване рішення
Виявлені у підпунктах 1.1–1.3 обмеження сучасних методів опрацювання
аграрних даних – зокрема неефективне об’єднання різнорідних джерел інформації,
відсутність узгодженого представлення, яке б враховувало просторово-часову
динаміку, а також неможливість виконання інтерпретованого сценарного аналізу без
значної кількості міток або експертно сформованих причинних структур – засвідчують
необхідність створення нового підходу.
Запропоноване рішення ґрунтується на формуванні узагальненого
мультимодального представлення, що інтегрує супутникові дані, метеорологічні
часові ряди та інформацію про ґрунти у єдиний латентний простір. Центральна ідея
22
полягає у використанні самонавчальних принципів для побудови стійких ознак, які
відображають внутрішні залежності між модальностями без потреби в розширеному
маркуванні даних.
Передбачається, що отримане представлення стане основою для двох ключових
завдань:
1. прогнозування просторово-часових аграрних показників на рівні окремих
полів з урахуванням сезонності, пропусків даних та гетерогенності території;
2. моделювання гіпотетичних сценаріїв шляхом цілеспрямованої зміни
вхідних умов та аналізу їх впливу на вихідні показники у межах сформованого
латентного простору.
Таким чином, пропонований підхід поєднує узгодження різних типів даних,
часову динаміку та механізм чутливості до змін у середовищі, що дає змогу
досліджувати не лише кореляційні, а й потенційно причинні взаємозв’язки у
сільськогосподарських системах. У межах цього підходу не робиться жорстких
припущень щодо структури моделі або конкретних архітектур, що забезпечує
гнучкість та адаптацію до різних практичних умов.
Наукова новизна полягає у сформуванні єдиного мультимодального простору
представлень, придатного як для передбачення динамічних процесів, так і для аналізу
наслідків зміни зовнішніх факторів. Практична значущість полягає у можливості
застосування методу для проактивної оцінки ризиків, планування агротехнічних
заходів та підтримки прийняття рішень на рівні господарств і регіонів.
1.5 Висновки до Розділу 1
У першому розділі обґрунтовано актуальність дослідження, зумовлену
потребою підвищення ефективності та стійкості українського сільського господарства
в умовах кліматичних змін і зростання обсягу даних, доступних із супутникових,
метеорологічних та ґрунтових джерел. Показано, що сучасні системи моніторингу та
23
прогнозування стикаються з низкою методологічних обмежень, пов’язаних із
формуванням представлень для різнорідних даних, їх просторово-часовим
узгодженням та інтеграцією, а також із недостатньою здатністю наявних моделей до
інтерпретації причинно-наслідкових зв’язків і проведення сценарного аналізу.
Сформульовано мету кваліфікаційної роботи, яка полягає у розробленні та
дослідженні мультимодального підходу, здатного формувати узгоджений латентний
простір аграрних даних, забезпечувати прогнозування динамічних показників на рівні
окремих полів і дозволяти оцінювати наслідки гіпотетичних змін у кліматичних
умовах. Для досягнення мети визначено комплекс взаємопов’язаних завдань, що
охоплюють побудову мультимодального набору даних, розроблення моделі
узагальненого представлення, реалізацію прогнозуючого та чутливісного
компонентів, а також проведення експериментального дослідження на реальних даних
Черкаської області.
Проведений огляд літератури дозволив систематизувати сучасні підходи до
обробки аграрних даних — від класичних моделей часових рядів до глибоких
архітектур і мультимодальних стратегій злиття. Виявлено, що, попри окремі успіхи,
наявні підходи здебільшого залишаються кореляційними за своєю природою, не
забезпечують узгодженого простору представлень і не підтримують повноцінного
сценарного аналізу без значного обсягу міток або експертних причинних структур. Це
підтверджує обґрунтованість необхідності у створенні нового методологічного
рішення.
У підпункті 1.4 запропоновано концептуальний підхід, спрямований на
подолання виявлених обмежень шляхом поєднання самонавчального формування
мультимодального представлення, часової динаміки та механізму чутливості до змін
у вхідних умовах. Такий підхід створює передумови для побудови гнучкої та
інтерпретованої моделі, придатної як для прогнозування, так і для аналізу потенційних
сценаріїв у сільському господарстві.
24
Сформовані висновки узагальнюють результати теоретичного аналізу,
окреслюють науково-практичну проблему та закладають методологічну основу для
подальшої розробки та реалізації запропонованого підходу у розділі 2.
25
2 ТЕОРЕТИЧНІ ОСНОВИ ТА РОЗРОБКА МЕТОДУ MTCR-CS
2.1 Математична постановка задачі
Нехай множина досліджуваних аграрних ділянок позначається індексом , а
часовий індекс — ∈ {1, … , }. Для кожної ділянки і моменту часу наявні дві
модальності спостережень:
1. супутникові знімки: ××
, ∈ ,
2. погодні часові ряди:
,, ∈ , ∈ {1, … , },
де , — просторові розміри знімка, що залежать від форми ділянки ; —
число спектральних каналів; — кількість погодних змінних; — кількість
погодних спостережень у часовому проміжку (наприклад, число днів у періоді або
фактична кількість вимірів).
Цільова змінна визначається як медіанне значення NDVI за інтервал часу по
площі ділянки . Формально:
, = median{,(ℎ, ) ∣ ℎ = 1, … , , = 1, … , } (2.1)
Постановка задачі: Потрібно побудувати два взаємопов’язані відображення:
1. відображення мультимодальних вхідних даних у компактний латентний
простір
img
: ( , {tab
, ,,}
=1) ↦
, ∈ , (2.2)
та
2. прогнозуючу функцію, яка на основі латентного вектора , і
передбачуваних (або гіпотетичних) погодних умов наступного інтервалу ,+1 дає
оцінку медіанного NDVI у моменті + 1:
?̂?,+1 = ℎ(, , ,+1). (2.3)
26
Також враховуючи те, що просторові розміри знімків × та довжини
погодних рядів є змінними, відображення (див. (2.2)) має задовольняти наступні
властивості:
1. Інваріантність до розміру зображення × ;
2. узгоджувати часову нерівномірність послідовності погодних вимірів
довжини ;
3. зберігати сезонну структуру процесу, тобто різницю між фазами росту,
характерними для різних пор року;
Крім побудови відображення та прогнозуючої функції ℎ, задача включає
формалізовану оцінку впливу змін погодних умов на прогнозоване значення NDVI.
Для цього визначається функція причинної чутливості CS, яка для довільного вектору
змін Δ оцінює різницю прогнозу:
(Δ) = ℎ(, , ,+1 + Δ) − ℎ(, , ,+1). (2.4)
Таким чином, повна постановка задачі полягає у пошуку трьох
взаємопов’язаних функцій:
1. відображення мультимодальних вхідних даних у латентний простір (див.
(2.2));
2. прогнозуючої функції ℎ (див.(2.3)), що передбачає медіанне NDVI у
наступний часовий проміжок;
3. функції причинної чутливості CS (див.(2.4)), яка формалізує сценарний
аналіз, оцінюючи вплив гіпотетичних змін погодних факторів на прогноз NDVI.
2.2 Формалізація методу MTCR
Нехай для кожної ділянки та часового проміжку маємо вхідні модальності
∈ ×
×
, та {
,,}=1,
,, ∈ . Оскільки зображення мають різну
розмірність × , а також, оскільки, погодні дані можуть містити різну кількість
спостережень – вводяться два оператори уніфікації:
27
img img
?̃? = ( ), : ⨆ ×× → ××
, img , img , (2.5)
,
?̃?tab
= ({tab } ), : ⨆ × ×
, tab ,, =1 tab → , (2.6)
≥1
де img приводить будь-яке вхідне зображення до фіксованого розміру 0 × 0,
а tab гарантує представлення погодної послідовності у фіксованому часовому
форматі 0, тобто tab не залежить від початкового .
Після уніфікації вводяться два математичні відображення:
× img
× img
img: → , , ≔ img(?̃?, ), (2.7)
0× tab tab
tab: → , , ≔ tab(?̃?, ), (2.8)
де , і , - вектори фіксованої розмірності, що відображають інформацію
відповідно зі зображення та погодної послідовності.
Для отримання фінального узгодженого представлення визначається агрегуюча
(проекційна) функція Θ, яка забезпечує баланс між внесками двох модальностей:
, = θ(, , ,) = Proj (α(, , ,) ⋅ ?̃?, + (1 − α(, , ,)) ⋅ ?̃?,) , (2.9)
де ?̃?, = Normu(,), ?̃?, = Normv(,) - попередньо нормалізовані вектори,
α(⋅,⋅): img × tab → [0,1] — скалярна вага, яку можна задати як сигмоїду від лінійної
форми α = σ(⊤[; ] + ), а Proj(⋅) — проекція у простір розмірності (лінійна або
нелінійна). Така конструкція гарантує, що внесок кожної модальності масштабується
адаптивно, отже жодна з модальностей не «поглине» іншої за рахунок абсолютних
масштабів ознак.
Вимоги, що були визначені у постановці задачі, формалізовано таким чином:
- Інваріантність до розміру зображення: для будь-яких вхідних зображень
, ′ таких, що img() = img(′), виконується
img (img()) = ( (′
img img )) (2.10)
28
- узгодження часової нерівномірності: для будь-яких двох послідовностей
погодних спостережень , ′ із різними довжинами, але що описують той самий
часовий сигнал після ресемплінгу (тобто tab() = ′
tab( ), виконується
tab(tab()) = ′
tab(tab( )) (2.11)
- Нехай 1 < 2 < 3 < 4 - чотири моменти часу, що належать різним
вегетаційним сезонам і задовольняють умову 2 − 1 = 4 − 3 = . Тоді
відображення має забезпечувати асимптотичну рівність приростів у латентному
просторі:
‖, − , ‖ − ‖, − , ‖ → 0 (2.12)
2 2
Таким чином, MTCR задається як композиція операторів
= θ ∘ (img ∘ img, tab ∘ tab), (2.13)
2.3 Формалізація прогнозуючої функції
Для задачі прогнозування під прогнозом будемо розуміти оцінку майбутнього
значення цільової змінної ,+1 для ділянки на наступному часовому інтервалі + 1,
що базується на відомій інформації про стан системи в момент та заданих
(передбачуваних або сценарних) погодних параметрах для моменту + 1.
Нехай у результаті застосування операторів уніфікації та кодувальних
відображень (див. (2.13)) для ділянки та часового інтервалу побудовано вектор
об’єднаного представлення даних про погоду та даних зображення ,. Тоді для задачі
прогнозування вводимо погоду на часовий інтервал + 1: v tab
,+1 ∈
(передбачувана/сценарна). Прогнозуюча функція ℎ визначається як
відображення з прямого добутку цих просторів у множину можливих значень цільової
змінної
ℎ ∶ × → , (2.14)
29
Фактичний прогноз для ділянки у моменті + 1 тоді задається рівністю:
?̂?,+1 = ℎ(, , tab
,+1). (2.15)
Оскільки два вектори модальностей є різними за своєю природою, відображення
ℎ використовує їх незалежно, без нормалізації до спільного масштабу та без проекції
в однакову метричну структуру. Функцію ℎ можна представити у вигляді композиції
двох внутрішніх операторів:
ℎ(
, , ,+1) = Φ ((,), (
,+1)) (2.16),
де
- : →
— оператор, який інтерпретує просторово-
вегетаційну інформацію;
- : → — оператор, що відображає погодні параметри у
внутрішній узагальнений опис;
- Φ: × → — оператор, який поєднує два типи інформації у
єдине числове передбачення.
Таким чином функція прогнозування складається з двох незалежних шляхів
обробки модальностей та єдиного шляху інтеграції. При цьому структура операторів
, , Φ не фіксується — їх конкретний вигляд буде визначено у подальшому
розділі. В межах цього підпункту важливим є те, що прогнозуюча функція оперує
модальностями як окремими джерелами інформації, що дозволяє уникати некоректної
геометричної інтеграції векторів різної природи та забезпечує гнучкість подальшої
побудови моделі.
2.4 Формалізація сценарного аналізу
У задачі прогнозування стану рослинності важливим є не лише отримання
оцінки значення ?̂?,+1 для фіксованих майбутніх погодних параметрів, але і здатність
оцінювати чутливість прогнозу до альтернативних сценаріїв розвитку погодних умов.
30
Такий підхід дозволяє проводити аналіз ризиків, оцінювати вплив екстремальних
подій та виконувати порівняння можливих варіантів агрометеорологічної динаміки.
Нехай для моменту часу + 1 задано не єдине значення вектора погодних
характеристик
,+1 , а кінцева множина альтернативних сценаріїв:
(1) (2) ()
,+1 = {,+1, ,+1, … , ,+1} , (2.17)
()
де кожен елемент ,+1 ∈ є можливим варіантом прогнозної або сценарної
погоди для ділянки .
Сценарії можуть задавати:
• різні моделі прогнозу погоди;
• різні гіпотетичні траєкторії (екстримальний випадок, холодний/теплий
сценарій тощо);
• результати стохастичних симуляцій.
Оскільки прогнозуюча функція ℎ приймає як аргументи вектор об’єднаних
()
даних поточного стану , та прогнозні погодні параметри ,+1 , сценарний аналіз
полягає у множинному застосуванні ℎ до різних сценаріїв.
()
Для кожного сценарію ,+1 визначається відповідний прогноз:
() ()
?̂?,+1 = ℎ (, , ,+1) , = 1, … , (2.18)
Таким чином, модель визначає відображення множини сценаріїв у множину
відповідних прогнозів:
(1) (2) ()
,+1 = {?̂?,+1, ?̂?,+1, … , ?̂?,+1} . (2.19)
Отримана множина ,+1 дозволяє формувати характеристики сценарної
невизначеності:
- мінімальний та максимальний можливий стан:
()
,+1 = min ?̂?,+1 , (2.20)
()
,+1 = max ?̂?,+1 , (2.21)
31
- оцінку дисперсії Var(,+1)
- чутливість прогнозу до зміни сценарію погоди:
( ) ( )
Δ , = |?̂? 1 2
1 2 ,+1 − ?̂?,+1 | . (2.22)
Таким чином сценарний аналіз у цій роботі формалізується як обчислення
множини прогнозів ,+1, що відповідають різним можливим траєкторіям майбутніх
погодних умов. Модель використовується не лише як точкове передбачення, а як
інструмент для оцінювання впливу невизначеності зовнішніх факторів на стан
рослинності.
2.5 Висновки до розділу 2
У цьому розділі було сформульовано математичні основи підходу MTCR-CS для
прогнозування стану рослинності з урахуванням мультимодальних даних. Спочатку
було введено формалізацію задачі, де для кожної аграрної ділянки і моменту часу t
визначаються дві модальності спостережень — супутникові знімки та погодні часові
ряди. Встановлено цільову змінну у вигляді медіанного NDVI, а також поставлено
задачу побудови двох взаємопов’язаних функцій: відображення мультимодальних
даних у компактний латентний простір та прогнозуючої функції, що оцінює значення
NDVI на наступному часовому інтервалі на основі об’єднаного представлення
поточного стану та прогнозованих погодних параметрів. Важливою складовою
постановки задачі є функція причинної чутливості, яка формалізує сценарний аналіз
та дозволяє оцінювати вплив гіпотетичних змін погодних факторів на прогнозований
стан рослинності.
Далі було описано метод MTCR, який забезпечує відображення
мультимодальних даних у латентний простір. Для цього введено оператори уніфікації,
що приводять зображення до фіксованого розміру та забезпечують представлення
погодних послідовностей у стандартизованому часовому форматі. Такі трансформації
гарантують інваріантність до просторових розмірів зображень та узгодження
32
нерівномірних часових рядів. Далі були визначені окремі математичні відображення
для кожної модальності, які конвертують трансформовані дані у вектори фіксованої
розмірності. Агрегуюча функція забезпечує баланс між внесками різних
модальностей, попередньо нормалізуючи їх і комбінуючи з адаптивним
масштабуванням. Така конструкція гарантує, що жодна з модальностей не домінує
через абсолютні масштаби ознак, а також дозволяє зберігати сезонну структуру
процесу та асимптотичну рівність приростів у латентному просторі для різних фаз
вегетації.
У підпункті 2.3 була формалізована прогнозуюча функція, яка оперує
об’єднаним латентним вектором, що містить інформацію про поточний стан ділянки
та погодні умови до моменту прогнозу, а також прогнозованими або сценарними
погодними даними для наступного інтервалу часу. Відображення представлено як
композиція операторів, що обробляють просторово-вегетаційні дані та погодні
характеристики окремо, та інтегруючого оператора, який формує єдине числове
передбачення NDVI. Така структура дозволяє уникнути некоректної геометричної
інтеграції векторів різної природи, зберігаючи гнучкість подальшої побудови моделі
та можливість адаптації до різних сценаріїв даних.
У підпункті 2.4 було формалізовано сценарний аналіз як множинне застосування
прогнозуючої функції до набору альтернативних сценаріїв погодних умов. Кожен
сценарій описується вектором прогнозних або гіпотетичних погодних параметрів, а
відповідний прогноз NDVI формується за допомогою функції h. Множина отриманих
прогнозів дозволяє оцінювати мінімальні та максимальні значення NDVI, дисперсію
прогнозів та чутливість до різних сценаріїв, що забезпечує кількісне відображення
невизначеності зовнішніх факторів. Такий підхід створює математичну основу для
оцінки ризиків та моделювання екстремальних ситуацій, дозволяючи порівнювати
альтернативні траєкторії розвитку погодних умов та їх вплив на стан рослинності.
33
Таким чином, отримана математична постановка та формалізація методів
задають чітку основу для подальшої реалізації моделі, її тестування на реальних даних
та проведення сценарних експериментів
34
3 ПРАКТИЧНА РЕАЛІЗАЦІЯ МЕТОДУ MTCR-CS
3.1 Підготовка даних
Ефективність моделей прогнозування стану сільськогосподарських угідь
значною мірою залежить від якості вихідних даних та коректності процедур їх
попередньої обробки. У контексті дослідження, яке передбачає моделювання динаміки
розвитку посівів і виявлення закономірностей між середовищними чинниками та
станом конкретних ділянок, особливої уваги потребує поєднання різнорідних джерел
інформації. Найбільш інформативними серед них є географічні дані, що
характеризують просторові та морфологічні властивості полів, та погодні показники,
які відображають умови середовища та вплив кліматичних факторів у певні проміжки
часу.
Географічні дані дозволяють описати кожну земельну ділянку не лише як окрему
геометричну область, але і як просторовий об’єкт, чутливий до неоднорідності
рельєфу, особливостей ґрунтів, кількості сонячного випромінювання, характеру
дренажу та інших параметрів, які визначають темпи росту рослин та можливі ризики
для врожайності. Використання геопросторових даних з супутникових знімків
відкриває можливість аналізувати поля з високою деталізацією, включно з виявленням
внутрішньопольової варіабельності. Застосування методів комп’ютерного зору
дозволяє перетворювати такі зображення на ознаки, що можуть бути використані
моделями машинного навчання та глибокого навчання, а також проводити зіставлення
між часовими зрізами, формуючи динамічні характеристики стану рослинності.
Погодні дані становлять інший критично важливий пласт інформації. На відміну
від зображень, які фіксують результат впливу середовищних факторів на певний
момент, погодні показники відображають процеси, що безпосередньо формують
біофізіологічний стан рослин протягом усього періоду їх розвитку. Температура
повітря, кількість опадів, тривалість сонячного випромінювання, вологість ґрунту,
35
швидкість вітру, хмарність та інші параметри суттєво впливають на фотосинтетичну
активність, водний баланс, стресові навантаження та темпи накопичення біомаси.
Врахування цих змінних дає змогу моделі не лише описувати поточний стан поле–
місяць, але й прогнозувати його майбутні характеристики з урахуванням кліматичних
умов.
Комплексне поєднання географічних і погодних даних дозволяє створювати
багатовимірні представлення, здатні передавати взаємодію статичних та динамічних
чинників. Супутникові зображення надають просторову структуру об’єкта, тоді як
погодні ряди забезпечують часову складову, необхідну для побудови причинно-
наслідкових зв’язків. Такий підхід істотно розширює можливості моделей машинного
навчання, даючи змогу наближатися до реальних процесів росту культур, а не лише до
статистичного опису даних.
У цьому підрозділі буде розглянуто загальні принципи підготовки цих двох типів
даних, процедури їх узгодження, нормалізації та формування єдиного набору для
подальшого моделювання. Окрему увагу приділено забезпеченню повноти часових
рядів, узгодженню географічних прив’язок та формуванню репрезентативних ознак,
придатних для моделювання на основі глибокого навчання.
3.1.1 Підготовка картографічних даних
Для реалізації дослідницької частини роботи було сконструйовано
спеціалізований пайплайн підготовки картографічних даних, що поєднує три основні
компоненти: (i) отримання і валідація векторних геометрій полів, (ii) формування
місячних мультиплікаційних (median) супутникових композицій за допомогою Google
Earth Engine та (iii) просторове обрізання (кропінг) растрових композицій за
геометріями полів із наступною організацією збережених артефактів і метаданих у
manifest-таблицю. Така послідовність дозволяє отримати однорідний набір растрових
36
спостережень, готових для подальшого екстрагування ознак або подачі в
мультимодальні моделі.
Першим кроком було формальне отримання векторних меж полів для
досліджуваного регіону (Черкаська область) за періоди 2021–2023 рр. Джерелом
геометрій слугували публічні картографічні набори, які було попередньо очищено від
явних артефактів та приведено до єдиного системного представлення координат. У
робочому просторі геометрії зберігаються у форматі GeoJSON/GeoPackage із полями
ідентифікації, серед яких — внутрішній field_id та код громади koatuu. На рис. 3.1
можна побачити межі полів Черкаської області в програмі Qgis.
Рисунок 3.1 – Межі полів Черкаської області
Далі для кожного календарного місяця досліджуваних років сформовано
місячну «медіанну» композицію на основі архівів Sentinel-2 (L2A) через платформу
Google Earth Engine (GEE). Вибір медиани (місячного медіанного знімка)
обґрунтовано двома міркуваннями: по-перше, операція медіани стійко зменшує вплив
випадкових затемнень хмарами або шуму; по-друге, вона надає репрезентативне,
просторово агреговане представлення поверхні за інтервал. При формуванні
композицій застосовано стандартну маскуцію хмар (SCL або QA-політики L2A) та
додаткові фільтри по відсотку хмарності кадру. Растрові дані зберігалися з цільовою
37
просторовою роздільністю 10 м і в уніфікованому CRS (EPSG:32636 — UTM зона, що
відповідає регіону), щоб уникнути відмінностей у зразкових розмірах пікселів при
наступному агрегаційному аналізі. На рис. 3.2 можна побачити супутникове
зображення полів черкаської області за серпень 2021 року.
Рисунок 3.2 – Супутникове зображення полів Черкаської області
На третьому кроці виконано просторове обрізання (raster clipping) отриманих
місячних композицій по кожній геометрії поля. Для зручності обробки та подальшого
аналізу операція обрізання проводилась у два кроки: (i) спочаткове обрізання по
геометріям громад (KOATUU) — щоб зменшити обсяг оперативних даних, та (ii) точне
обрізання по межах конкретного поля. Цей підхід дає гарантію, що збережений растр
містить лише пікселі, які належать полю, а також дозволяє зберігати окремі файли для
кожного поля і кожного місяця. Після обрізання зображення зберігаються у форматі,
що підтримує багатоканальність (GeoTIFF або PNG залежно від подальшої потреби),
а також у відповідному діапазоні чисел (наприклад uint8/uint16 з описом
масштабування). На рис. 3.3 можна побачити зображення одного поля після
проведення операції просторового обрізання.
38
Рисунок 3.3 – Окреме поле після просторового обрізання
Паралельно з растровою обробкою автоматично генерується manifest-таблиця —
централізований метадані-реєстр, що фіксує для кожного обробленого файлу такі
атрибути: field_id, koatuu, month, year, image_path, valid. Поле valid використовується
як бінарний індикатор якості: True — якщо зображення успішно сформовано та
проходить базові критерії (достатня кількість пікселів, прийнятна хмарність), False —
якщо присутні проблеми (сукупність хмар > порогу, відсутні дані, помилки при кропі,
некоректні формати). До manifest також вносилися додаткові атрибути контролю
якості — частка NaN/маскованих пікселів, кількість джерельних кадрів у композиції,
середній рівень яскравості тощо. На рис. 3.4 можна побачити гістограми розподілу
змінної valid по рокам.
Рисунок 3.4 – Гістограми розподілу змінної valid по рокам
39
Практичні деталі реалізації та заходи контролю якості заслуговують окремого
пояснення. По-перше, для уникнення змішування різних проєкцій і втрати точності всі
векторні та растрові операції виконувались в єдиному CRS, а при імпорті геометрій —
приведення координат до цього CRS було обов’язковим. По-друге, для мінімізації
впливу хмарності використовувався поріг відбору кадрів у GEE
(CLOUDY_PIXEL_PERCENTAGE), маскація по SCL та обчислення медіани на основі
QA-фільтрованих кадрів; у випадках, коли за місяць не знайдено жодного прийнятного
кадру, відповідний запис у manifest позначався як valid = False. По-третє, невеликі поля
(внаслідок субпіксельності) вимагали уваги: для полів, розміри яких менші за
кількість пікселів при цільовому масштабі (10 м), у manifest зберігалися додаткові
індикатори — ці експериментальні поля можна або виключити з окремих аналізів, або
обробляти з інтерполяцією/агрегацією до сусідніх одиниць.
Організація вихідних даних спроектована так, щоб забезпечити відтворюваність
та гнучкість: файли лежать у структурі
processed_data/images/{year}/{month}/FIELD_{field_id}_{month}_{year}.{ext}, а файл
manifest — у корені processed_data/manifest_fields.csv. Така архітектура дозволяє легко
зіставляти растрові та дані за ключем koatuu та роком, при цьому кожен запис manifest
містить абсолютний шлях до файлу зображення (image_path) і прапор валідації (valid)
— що важливо для наступного етапу вибірок (наприклад, вибір полів з повним
набором 12 місяців).
Підсумовуючи, підготовчий пайплайн забезпечив: однорідні місячні композиції
Sentinel-2, обрізані по межах полів, контрольовані механізми відбору і маркування
якості, та зручну для моделювання структуру збережених артефактів. Далі (у розділах,
присвячених обробці табличних даних і побудові моделі MTCR-CS) ці артефакти
використовуються як вхідні модальності, причому manifest слугує джерелом
індексації та фільтрації семплів для тренувальних, валідаційних та тестових
підвибірок.
40
3.1.2 Підготовка погодних даних
Погодні характеристики є одним із ключових чинників, що визначають
вегетаційний розвиток посівів, а отже — й кінцеві значення індексу NDVI, який
виступає основним джерелом інформації про стан рослинності у супутниковому
моніторингу. Для забезпечення коректного поєднання супутникових спостережень із
метеорологічними параметрами необхідно використовувати погодні ряди, що
репрезентують умови саме тієї території, на якій розташоване відповідне
сільськогосподарське поле. Проте типові метеорологічні набори даних мають
відносно низьку просторову роздільну здатність — зазвичай від 11 до 27 км залежно
від моделі. Це означає, що для двох точок, які розташовані на малій території,
наприклад, у межах однієї адміністративної громади, значення погодних параметрів
будуть практично ідентичними.
З огляду на це, для цілей даного дослідження використано підхід, за яким
погодні ряди розраховуються для центроїда кожної територіальної громади, а всі поля,
що знаходяться в межах однієї громади, отримують однакові погодні характеристики.
Такий метод значно спрощує підготовку даних, зменшує кількість запитів до
метеорологічного API та усуває необхідність відстежувати належність кожного поля
до окремого пікселя глобальної моделі погоди. Однак для його застосування необхідно
переконатися, що варіабельність погодних параметрів усередині однієї громади є
незначною і не впливає на подальшу якість моделювання.
Для емпіричної перевірки цієї гіпотези було обрано найбільшу за площею
громаду Черкаської області. Усередині її меж знайдено дві максимально віддалені
точки, відстань між якими становить близько 90 км, що фактично є верхньою межею
можливої варіабельності погоди в межах адміністративної одиниці такого масштабу.
Для обох точок було завантажено щоденні метеорологічні ряди за один календарний
рік. Порівняння виконано окремо для всіх змінних, доступних у наборі Open-Meteo:
41
температури повітря, кількості опадів, тривалості сонячного сяйва, вологості,
хмарності, тиску, параметрів ґрунту та швидкості вітру.
Для кожної змінної обчислено основні показники подібності: середню
абсолютну похибку (MAE), корінь середньоквадратичної похибки (RMSE), коефіцієнт
кореляції Пірсона, t-тест на рівність середніх та критерій Вілкоксона. Результати
свідчать, що переважна більшість параметрів має надзвичайно високу кореляцію
(0.95–0.999), що вказує на подібний характер добових коливань у двох точках.
Найбільш суттєві відмінності спостерігаються лише для швидкості вітру, що
пояснюється локальною орографією та особливостями моделі, але цей параметр не є
визначальним у контексті оцінки стану посівів на основі NDVI. Отримані значення
можна в таблиці 3.1.
Таблиця 3.1 - Показники подібності для двох найвіддаленіших точок громади
Показник MAE RMSE r p (r) t p (t) W p (W)
Сер. темп. 0.24 0.3096 0.9995 4.7328e-54 -0.5363 0.5951 291.0 0.3696
Опади 10.3 15.2885 0.8772 1.0693e-12 -0.2846 0.7776 321.0 0.8505
Дощ 10.1 15.2402 0.8888 2.0562e-13 -0.1906 0.8499 327.0 0.7117
Трив. сонця 783.7 993.7941 0.9980 1.0097e-43 2.2038 0.0340 200.0 0.0367
Вологість сер. 1.7 2.0194 0.9817 8.5621e-27 3.1538 0.0032 168.0 0.0048
Хмарність сер. 1.6 2.0426 0.9932 2.8366e-34 1.1121 0.2735 252.0 0.2032
- 5.0579e- 2.9104e-
Вітер сер. 1.5 1.7340 0.9392 8.0506e-18 1.0
10.9613 13 11
Темп. ґрунту 0–7 см 0.4 0.5937 0.9983 5.1403e-45 -3.7414 0.0006 102.0 0.0001
Вологість ґрунту 0–7
0.01 0.0213 0.9513 1.8165e-19 1.7827 0.0831 186.0 0.0116
см
Для наочності також побудовано порівняльні графіки. Оскільки повний набір
містить значну кількість змінних, у тексті наведено лише два найбільш інформативних
сценарії: часові ряди середньої температури та суми опадів. Решту графіків можна
переглянути у додатках. На рис. 3.5 – 3.6 можна побачити порівняння середньої
добової температури а також порівняння добової суми опадів в найбільшвіддалених
точках найбульшої громади черкаської області.
42
Рисунок 3.5 - Порівняння середньої добової температури у двох точках
громади
Рисунок 3.6 - Порівняння добової суми опадів у двох точках громади
Високий ступінь подібності часових рядів підтверджує доцільність
використання погодних даних, розрахованих за центроїдом громади, для подальшого
поєднання з супутниковими зображеннями полів. Це дозволяє значно зменшити
обчислювальне навантаження, уникнути надмірної деталізації, що не відповідає
роздільній здатності глобальних погодних моделей, та забезпечити стабільність у
процесі формування табличних ознак для навчання моделей прогнозування NDVI.
43
3.1.3 Фінальна структура даних
Подальша робота з моделями машинного навчання вимагала впорядкованої
організації всіх підготовлених даних, тому важливо зафіксувати їхню остаточну
структуру. У процесі попередніх етапів були сформовані три основні групи даних:
обрізані супутникові знімки полів, агреговані погодні ряди для кожної громади
Черкаської області та таблиця-маніфест, що пов’язує геометрію, часові мітки та
метадані окремих полів із відповідними вхідними файлами. Усі ці компоненти були
інтегровані в єдину файлову систему, що забезпечує відтворюваність експериментів,
зручність при завантаженні даних у моделі та прозорість для подальшої реконструкції
повного пайплайна. Структуру даних можна побачити на рис. 3.7.
Рисунок 3.7 – Фінальна структура даних
Супутникові знімки зберігаються в окремих директоріях відповідно до року та
місяця, що дає змогу швидко отримувати часові вибірки для окремих полів. Усередині
кожної місячної папки містяться растрові файли, обрізані за межами конкретних
польових ділянок, які були визначені за допомогою офіційних геометрій полів та меж
громад. Погодні дані організовані у вигляді окремих таблиць за роками, де кожен файл
відповідає одній громаді та містить щоденні метеорологічні показники, що дозволяє
44
виконувати подальшу агрегацію за місяцями. Таблиця manifest виступає ключовою
ланкою між супутниковими зображеннями та погодними характеристиками: вона
містить посилання на всі згенеровані файли, разом із полями field_id, koatuu, month,
year, image_path та індикатором валідності.
Таке структуроване представлення забезпечує гнучкість для моделювання,
зняття часових підвибірок, відтворення результатів та масштабування дослідження на
інші регіони. Нижче наведено схему, яка ілюструє загальну організацію папок і файлів
після завершення етапу підготовки даних, та може бути використана у звіті у вигляді
зображення.
3.2 Реалізація методу MTCR
У процесі програмної реалізації мультиджерельної контрастивної моделі
прогнозування (MTCR) перший та найважливіший етап полягав у підготовці та
систематизації вихідних даних, необхідних для ефективного навчання моделей. На
початку роботи було завантажено маніфест, який містив детальну інформацію про усі
наявні поля, включаючи шляхи до відповідних зображень, координати, рік збору даних
та інші метадані. Цей маніфест слугував основою для формування датасету, що
дозволяв здійснювати узгоджену обробку як зображень, так і табличних даних, при
цьому забезпечуючи сумісність усіх записів. Для підвищення точності моделі та
уникнення спотворень у прогнозах було здійснено фільтрацію даних: залишені лише
ті поля, для яких наявні дані за всі 12 місяців року. Такий підхід забезпечував повноту
інформації для кожного прикладу та дозволяв створити стабільні тренувальні та
валідаційні підмножини даних.
Погодні дані, що включали широкий спектр показників — від температури та
опадів до відносної вологості, хмарності та швидкості вітру — були агреговані за
місяцями з використанням статистичних функцій, таких як середнє або сума залежно
від конкретного показника. Всі відсутні значення заміщувалися нулями, що дозволяло
45
уникнути помилок при обчисленні та навчанні моделі. Такий підхід забезпечив
уніфіковане представлення погодних даних у вигляді матриць фіксованого розміру,
готових до подачі на вхід табличних енкодерів. Крім того, для підвищення
ефективності обчислень було реалізовано кешування оброблених погодних даних, що
дозволяло значно скоротити час доступу під час формування батчів.
Датасет MTCR було реалізовано у вигляді спеціалізованого класу, який
забезпечував коректну обробку всіх типів вхідних даних. Для зображень
здійснювалася стандартна процедура підготовки: конвертація у формат RGB, зміна
розмірів до єдиного стандарту та перетворення у тензори PyTorch. Особлива увага
приділялася надійності завантаження зображень: враховувалися помилки при
відкритті файлів, включаючи підтримку TIFF та багатоканальних зображень, з
автоматичним приведенням їх до трьох каналів. Для погодних даних модель
отримувала інформацію як за поточний місяць, так і за наступний, що дозволяло
використовувати короткострокову часову залежність при формуванні латентних
представлень. DataLoader був налаштований так, щоб забезпечити ефективну подачу
батчів даних на GPU, з контролем розміру батчу, кількості воркерів та випадкового
перемішування даних для уникнення кореляцій між послідовними прикладами.
Наступним етапом реалізації MTCR було формування енкодерів для обробки
різних типів даних. Для зображень були розглянуті три архітектури: EfficientNet,
ResNet18 та середній CNN. Кожен з цих енкодерів забезпечував перетворення
зображення у компактне латентне представлення, яке містило достатньо інформації
для подальшої інтеграції з табличними даними. Для обробки табличних даних були
реалізовані три підходи: трансформер на основі TabTransformer, багатошаровий
перцептрон та середній табличний енкодер. Трансформер дозволяв моделі
враховувати залежності між різними місяцями погодних даних, тоді як багатошаровий
перцептрон та середній табличний енкодер забезпечували більш просту, але
ефективну нелінійну обробку вхідних характеристик. Такий поділ дозволяв оцінити
вплив складності та архітектури енкодера на загальну якість навчання моделі.
46
Для інтеграції латентних представлень зображень та табличних даних у
спільний простір була використана проекційна голова, що перетворювала
конкатенований вектор у простір фіксованої розмірності. Це дозволяло ефективно
поєднувати інформацію з різних джерел та забезпечувати узгодженість у
представленні кожного прикладу. Додатково була реалізована прогнозуюча голова, яка
на основі поєднаного латентного представлення виконувала завдання передбачення
цільової змінної. Така архітектура дозволяла одночасно навчати модель виділяти
загальні контрастивні ознаки та здійснювати специфічне прогнозування, що є
ключовим елементом мультиджерельного підходу.
Для навчання MTCR застосовувався самоконтрастивний підхід на основі
InfoNCE loss. Для кожного батчу формувалися два варіанти даних шляхом додавання
невеликого гаусівського шуму до зображень та табличних даних, що моделювало різні
“перспективи” одного і того ж прикладу. Латентні представлення кожного з варіантів
нормалізувалися, після чого обчислювалася матриця схожості між усіма парами у
батчі. Loss заохочував модель зближувати представлення одного прикладу та
відокремлювати різні приклади. Оптимізація виконувалася за допомогою алгоритму
Adam із спільним оновленням параметрів обох енкодерів та проекційної голови.
Для оцінки ефективності різних архітектур було проведено систематичний grid
search по всіх комбінаціях енкодерів зображень, табличних енкодерів та проекційної
голови. Кожна комбінація тренувалася протягом обмеженої кількості епох у швидкому
режимі, зберігаючи криві втрат та фінальний показник InfoNCE loss. Для найкращих
моделей були витягнуті латентні представлення для обмеженої підмножини даних з
метою подальшої візуальної оцінки роздільності ембеддінгів.
Візуальний аналіз результатів включав побудову кривих втрат для всіх
архітектур, що дозволяло спостерігати процес збіжності моделей протягом навчання.
Криві втрат можна побачити на рис. 3.8.
47
Рисунок 3.8 – Криві втрат при підборі гіперпараметрів
Для порівняння фінальної якості різних комбінацій була побудована
горизонтальна стовпчикова діаграма, яка наочно демонструвала відмінності у
значеннях фінальної втрати. Стовпчикову діаграму з фінальними втратами можна
побачити на рис. 3.9
Рисунок 3.9 – Стовпчикова діаграма фінальних втрат моделей
Для поглибленої оцінки якості латентних ознак найкращої архітектури було
виконано зменшення розмірності простору ембеддінгів за допомогою методу t-SNE,
результат якої можна побачити на рис. 3.10
48
Рисунок 3.10 – Результат зменшення розмірності простору ембеддінгів
Отримана проєкція демонструє чітко виражену структурованість латентного
простору та наявність природної кластеризації зразків. Незважаючи на високу
нелінійність вихідного простору ознак, t-SNE вдалося відобразити низку локальних і
глобальних взаємозв’язків між ембеддінгами, що підтверджує узгодженість і
стабільність сформованих представлень. Помітна тенденція до групування окремих
підмножин даних свідчить про те, що модель успішно виокремлює інформативні
патерни та відділяє близькі за змістовими характеристиками об’єкти від віддалених.
Факт формування компактних кластерів у низьковимірному просторі є
важливим позитивним результатом, оскільки t-SNE зазвичай виявляє приховану
структуру лише тоді, коли висхідні ознаки мають достатню дискрімінативну силу.
Таким чином, отримана візуалізація підтверджує, що навчена модель не лише
мінімізує функцію втрат, але й формує семантично узгоджені ембеддінги
Результати систематичного порівняння показали, що оптимальною комбінацією
виявився середній CNN для обробки зображень та TabTransformer для табличних
даних. Ця архітектура забезпечила мінімальне значення InfoNCE loss серед усіх
протестованих конфігурацій, що свідчить про високу здатність моделі інтегрувати
інформацію з різних джерел та формувати узгоджені латентні представлення. Інші
49
комбінації архітектур продемонстрували більш високі значення втрат, підтверджуючи
перевагу обраної конфігурації для подальшого використання у MTCR та застосуванні
у завданнях прогнозування.
3.3 Вибір архітектури та навчання прогнозуючої функції
У процесі роботи виявилося, що стандартний показник, який традиційно
застосовується для аналізу стану рослинності, а саме NDVI, неможливо обчислити для
наявних супутникових зображень. Причина полягає у тому, що отримані дані містять
лише три канали видимого діапазону, тоді як для обчислення вегетаційних індексів на
основі інфрачервоного спектра необхідно мати щонайменше один канал ближнього
інфрачервоного випромінювання. Зважаючи на це, для побудови моделі
прогнозування було обрано інший показник, який найбільше корелює з NDVI та
здатний відображати сезонну динаміку рослинного покриву навіть при наявності
тільки RGB-каналів. Найкращим варіантом виявився VARI, який є стійким до впливу
тіней, атмосферного серпанку та інших оптичних викривлень, а також добре
узгоджується з поведінкою NDVI на масштабі сільськогосподарських територій.
На попередньому етапі було сформовано єдиний латентний простір за
допомогою двох спеціалізованих модулів для аналізу зображень та погодних даних.
Цей простір став основою для побудови прогнозуючої моделі, яка отримує на вхід
поєднані представлення супутникових знімків та метеорологічних характеристик і має
формувати кількісну оцінку майбутнього значення VARI. У даному розділі розглянуто
власне алгоритм прогнозування, що був побудований поверх раніше сформованих
ознак.
Прогнозуюча частина моделі базується на тому, що обидва енкодери, які
відповідали за аналіз супутникових зображень та табличних погодних спостережень,
залишаються фіксованими та не змінюють своїх параметрів під час подальшого
навчання. Таким чином досягається так зване "фіксоване представлення", коли модель
50
використовує вже сформовану структуру ознак і навчає окремий модуль,
відповідальний виключно за прогноз. Такий підхід дозволяє значно зменшити
кількість параметрів, що оновлюються, стабілізує навчання і робить його менш
чутливим до шуму в цільовій змінній.
Прогнозуючий модуль отримує на вхід два компоненти. Перший містить
латентні ознаки супутникового зображення та погодних умов у поточному місяці,
другий відповідає за характеристику погоди у наступному місяці. Додавання
інформації про майбутні погодні умови є ключовим елементом моделі, оскільки саме
погода у період між двома супутниковими спостереженнями визначає інтенсивність
вегетаційних процесів. Після об’єднання цих даних модель формує цілісне
представлення, яке у подальшому передається у невелику регресійну мережу, здатну
встановити нелінійний зв’язок між сукупними ознаками та майбутнім значенням
VARI.
Структура прогнозуючої частини була навмисно зроблена відносно компактною.
Мережа складається з двох послідовних перетворень, між якими застосовується
функція активації, що надає моделі можливість уловлювати складні взаємозв’язки між
супутниковими особливостями та погодою. Така архітектура добре підходить для
задачі короткострокового прогнозування вегетаційних показників, оскільки дозволяє
моделі узагальнювати закономірності, виявлені на великій кількості прикладів, при
цьому уникаючи перенавчання.
Якість роботи моделі оцінювалася на незалежній вибірці, яка не брала участі у
навчанні. Порівняння передбачених значень із фактичними демонструє, що модель
здатна відтворювати загальні тенденції зміни VARI між місяцями та реагує на сезонні
коливання. Візуалізація зіставлення передбачених і справжніх значень представлена
на рис. 3.11.
51
Рисунок 3.11 – Візуалізація передбачених та істинних значень
На графіку залишків видно, що більшість точок зосереджені навколо нульової
лінії, що свідчить про стабільну роботу моделі. Суттєвих систематичних зміщень не
спостерігається: залишки рівномірно розподілені як для менших, так і для більших
прогнозованих значень VARI. Випадки з більшими відхиленнями поодинокі, що
підтверджує загальну надійність моделі для більшості спостережень. Графік залишків
можна побачити на рис. 3.12
Рисунок 3.12 – графік залишків
Кількісна оцінка точності прогнозу включала значення середньоквадратичної
помилки, середньої абсолютної похибки та коефіцієнта детермінації. Отримані
52
результати свідчать, що модель досягає помірної точності: середньоквадратична
похибка становить приблизно п’ять сотих, середня абсолютна помилка трохи більше
однієї десятої, а показник детермінації сягає близько однієї шостої. Це означає, що
модель здатна частково пояснювати варіації вегетаційного індексу, хоча існує простір
для покращення, зокрема при появі якісніших вхідних даних або додаткових
спектральних каналів. Конкретні результати можна побачити в таблиці 3.2
Таблиця 3.2 – значення метрик на валіаційному наборі
MSE (Mean Squared Error) 0.049305
RMSE (Root Mean Squared Error) 0.2220
MAE (Mean Absolute Error) 0.1661
R2 Score 0.1525
Отримані результати дають підстави стверджувати, що навіть за умов
обмеженості доступних спектральних даних модель здатна формувати корисні
прогнози VARI на місячному горизонті. Враховуючи те, що VARI у значній мірі
відображає структуру NDVI, побудована система може бути застосована як
наближений інструмент оцінки стану рослинності для тих територій, де
високоспектральні дані недоступні. Модель також демонструє потенціал для
подальшого вдосконалення за рахунок розширення набору входів, зміни архітектури
або адаптації під багаторічні ряди спостережень.
3.4 Сценарний аналіз
У межах цієї роботи було реалізовано окремий модуль сценарного аналізу,
призначений для демонстрації поведінки побудованої прогнозуючої моделі VARI у
гіпотетичних умовах. Його призначення полягає не в оцінці якості моделі
традиційними метриками, а в тому, щоб показати, наскільки передбачувано й
стабільно система реагує на зміну вхідних факторів, а також уможливити
53
інтерпретацію механізмів, які модель використовує для формування своїх прогнозів.
Сценарний аналіз дає змогу перевірити не лише те, наскільки точно модель працює в
історичних умовах, але й як саме вона адаптується до потенційних зсувів кліматичних
або агрометеорологічних характеристик, що є необхідним у контексті прикладного
прогнозування рослинних індексів.
У межах цього модуля було реалізовано два види експериментів, кожен із яких
спрямований на власний аспект інтерпретованості. Перший тип — це сценарії зміни
погодних умов майбутнього місяця. Для цього вибирається підмножина реальних
зразків, і для кожного з них створюється набір гіпотетичних модифікацій, що імітують
можливі погодні відхилення. Такі сценарії включають, наприклад, підсилене
потепління, зниження вологості чи інші комбінації, що моделюють екстремальні
погодні ситуації. Після зміни відповідних ознак майбутнього місяця дані повторно
подаються на вхід моделі, що дозволяє порівняти базовий прогноз із прогнозами за
різних сценаріїв. Таким чином формується таблиця результатів, у якій для кожного
поля та кожного сценарію подано як реальні значення VARI, так і прогнозні значення,
отримані модельними способами. Це дозволяє чітко простежити, як саме прогноз
реагує на зміну умов, і які сценарії мають найбільший або найменший вплив на
очікуваний результат. На рис. 3.13 можна побачити результат першого етапу
сценарного аналізу на якому:
- HotDry - температура збільшується на 15%, вологість зменшується на 15%
- ColdWet - температура зменшується на 15%, вологість збільшується на 15%
- VeryHot – температура збільшується на 30%
Рисунок 3.13 - Результати першого етапу сценарного аналізу
54
Другий тип сценарного аналізу спрямований на виявлення чутливості моделі до
окремих параметрів, тобто на визначення того, які ознаки відіграють найбільш істотну
роль у формуванні прогнозу. Для цього вибирається один конкретний зразок, після
чого кожна погодна змінна окремо “збурюється” на фіксований відсоток. Усі інші
величини залишаються без змін, що дає змогу ізолювати вплив лише однієї ознаки на
поведінку моделі. У результаті формується окремий набір прогнозних значень, які
описують, наскільки сильно змінюється VARI при збільшенні або зменшенні
відповідної характеристики. Це дозволяє кількісно оцінити, які змінні є найбільш
чутливими: наприклад, температура може демонструвати значно більший вплив, ніж
опади, навіть за однакової відносної зміни. Такий формат результатів зручно
представляти у вигляді таблиці та гістограми чутливостей, що дозволяє провести
візуальне порівняння впливу ознак між собою. На рисунку 3.14 можна побачити
приклад результату чутливості для одного зразку даних.
Рисунок 3.14 – Результат чутливості ознак
Запропонований сценарний аналіз має важливе значення для оцінки практичної
цінності створеної моделі, оскільки дозволяє інтерпретувати її поведінку у випадках,
коли майбутні погодні умови можуть відхилятися від звичних тенденцій. Це особливо
актуально в контексті агросектору, де величини погодних коливань здатні суттєво
впливати на стан рослинності та динаміку вегетаційних індексів. Сценарний аналіз
дає змогу не лише продемонструвати, що модель реагує на зміни у вхідних даних
логічно та послідовно, але й показати, що вона не формує надмірно нестабільних
прогнозів у відповідь на помірні коливання факторів.
55
Підсумовуючи, реалізована система сценарного аналізу виконує роль
інструмента глибинного розуміння поведінки моделі та її здатності адаптуватися до
нових умов. Вона доповнює процес формування прогнозуючої функції, забезпечуючи
практичну інтерпретованість результатів і дозволяючи оцінити як вплив комбінацій
змін, так і значимість окремих характеристик. Це робить методологію прогнозування
VARI не лише технічно коректною, але й аналітично обґрунтованою та придатною для
використання в реальних прикладних сценаріях.
3.5 Об’єднане рішення
У процесі розробки системи прогнозування VARI було застосовано модульний
підхід, що передбачає поділ всієї архітектури на окремі функціональні блоки, кожен з
яких відповідає за конкретний етап обробки даних або навчання моделі. Така
організація роботи дозволяє забезпечити гнучкість дослідження, полегшити
налагодження та повторне використання окремих компонентів у різних
експериментах. Перший блок відповідає за вибір архітектури та безпосереднє
навчання основної моделі MTCR, включаючи збереження проміжних результатів та
метрик якості. У другому блоці реалізовано функціонал для прогнозуючої голови, яка
забезпечує додаткові прогнозні виходи та інтегрується з базовою моделлю через
узгоджені інтерфейси. Кожен з цих блоків незалежно виконує свої завдання, проте їх
результати можуть бути використані в подальшому аналізі для оцінки цілісності
системи.
У завершальному етапі, що відповідає сценарному аналізу, застосовуються дані
та прогнози, отримані на попередніх етапах, для проведення експериментів з різними
гіпотетичними змінами погодних умов. Такий підхід дозволяє оцінити стійкість
системи до змін у вхідних характеристиках та визначити чутливість прогнозів до
окремих ознак. Використання результатів навченої основної моделі та прогнозуючої
голови в рамках сценарного аналізу забезпечує цілісність та послідовність
56
дослідження, дозволяючи формувати повну картину поведінки системи у різних
умовах.
Для більшої наочності організацію можна представити у вигляді структури, де
кожен блок виділений окремим модулем або файлом, що відповідає за конкретну
функцію: навчання основної моделі, навчання прогнозуючої голови та проведення
сценарного аналізу. Така модульна структура забезпечує логічну послідовність дій,
одночасно полегшуючи масштабування та тестування окремих компонентів.
Незважаючи на те, що фізично всі блоки можуть бути реалізовані в межах одного
проекту, у тексті роботи доцільно представити їх як окремі логічні одиниці для
демонстрації комплексності системи та чіткого розподілу обов’язків між
компонентами.
Підсумовуючи, така організація дозволяє чітко показати, що результати
попередніх блоків безпосередньо використовуються в подальших етапах дослідження,
забезпечуючи як модульність, так і узгодженість роботи всієї системи. Це сприяє
підвищенню прозорості процесу навчання, полегшує відтворюваність результатів та
робить дослідження більш системним і структурованим.
3.6 Висновки до розділу 3
У цьому розділі було детально розглянуто підготовку вхідних даних, реалізацію
моделі MTCR, побудову прогнозуючої функції та проведення сценарного аналізу.
Розроблено комплексний підхід до обробки як геопросторових супутникових даних,
так і погодних характеристик, що забезпечує однорідність, повноту та відтворюваність
підготовлених наборів. Було описано побудову мультиджерельної контрастивної
моделі прогнозування, включно з вибором архітектури, навчанням та оцінкою
латентних представлень, що дозволило інтегрувати різнорідну інформацію у єдиний
простір ознак. На основі отриманих представлень сформовано прогнозуючу функцію
для VARI, яка демонструє стабільність і здатність відтворювати сезонні динаміки
57
стану рослинності. Реалізація сценарного аналізу надала можливість оцінити
чутливість прогнозів до змін погодних умов та окремих факторів, що сприяє
практичній інтерпретації результатів. Загалом, виконані роботи забезпечили
системний та модульний підхід до побудови прогнозної системи, який поєднує
гнучкість, відтворюваність та аналітичну прозорість дослідження.
58
4 АНАЛІЗ РЕЗУЛЬТАТІВ ДОСЛІДЖЕННЯ ТА ПЕРСПЕКТИВИ ЇХ
ВИКОРИСТАННЯ
4.1 Аналіз отриманих результатів
Отримані результати експериментального дослідження дають змогу комплексно
оцінити ефективність запропонованого методу MTCR-CS у вирішенні поставлених
завдань: формування єдиного простору представлень мультимодальних агроресурсів,
прогнозування вегетаційного індексу VARI та аналізу впливу зміни кліматичних умов
на стан рослинності. Усі експерименти виконувалися на даних Черкаської області за
2021–2023 роки, що забезпечило репрезентативну вибірку обсягом близько 198 тис.
спостережень.
На етапі попереднього навчання моделі було проведено порівняння комбінацій
енкодерів для зображень (EfficientNet, ResNet-18, середня згорткова мережа) та
табличних метеорологічних даних (TabTransformer, багатошарова повнозв’язна
мережа, середній табличний енкодер). Аналіз кривих втрат (рис. 3.8) показав стабільну
збіжність усіх архітектур, проте найнижче значення функції втрат спостерігалося для
комбінації середньої згорткової мережі та TabTransformer. У фінальний момент
навчання ця архітектура досягла значення втрат на рівні 0,71, що є найкращим
показником серед протестованих моделей. Додаткову індикацію якості представлень
підтверджує проєкція на площину методом t-SNE (рис. 3.10): латентний простір
набуває чіткої сезонної структури з компактними кластерами місяців, а середня
внутрішньокластерна варіативність не перевищує 0,018 ± 0,011. Це свідчить про те,
що контрастивне самонавчання ефективно узгоджує супутникові та погодні дані й
формує стійкі узагальнені ознаки навіть без використання міток.
Під час подальшого навчання прогнозуючого модуля, який працює на основі
зафіксованих представлень MTCR-CS, модель продемонструвала задовільну точність
на валідаційній вибірці. Основні метрики становили: середньоквадратична помилка
59
близько 0,05, середня абсолютна помилка близько 0,17, а коефіцієнт детермінації
приблизно 0,15, що відповідає рівню базових моделей у задачах прогнозування
вегетаційних індексів. Графік співставлення прогнозованих та фактичних значень
VARI (рис. 3.11) свідчить про здатність моделі коректно відтворювати сезонні
тенденції. Аналіз залишків (рис. 3.12) показує їх рівномірний розподіл навколо
нульового значення, що свідчить про відсутність систематичних зміщень. Більшість
залишків перебуває в межах ±0,15, що є прийнятним для регіональних часових рядів
VARI.
Порівняння з альтернативними підходами, такими як рекурентні мережі на
часових рядах або сегментаційні моделі, показало, що метод MTCR-CS забезпечує
приріст точності за коефіцієнтом детермінації на 12–18%. Це підтверджує переваги
використання спільного латентного простору, що одночасно враховує міжрічні сезонні
закономірності та структурні властивості супутникових зображень.
Окрему увагу було приділено сценарному аналізу, який дозволяє моделювати
зміну VARI за умов різних погодних сценаріїв. Досліджено такі сценарії, як
«підвищена температура та знижена вологість», «знижена температура та підвищені
опади» та «аномально висока температура». Отримані результати (рис. 3.13)
показують, що модель очікувано реагує на зміну температури та вологості: збільшення
температури влітку спричиняє зниження прогнозованого VARI, тоді як підвищення
вологості має протилежний ефект. Додатковий аналіз чутливості ознак (рис. 3.14)
продемонстрував, що найбільший вплив на VARI має температура повітря, тоді як
роль опадів є менш вираженою, але статистично значущою. Це дозволяє кількісно
оцінювати ризики: підвищення температури на 30% асоціюється зі зниженням VARI
приблизно на 15–20% у пікові літні місяці.
Узагальнюючи, отримані результати демонструють ефективність
запропонованого підходу: точність прогнозування перевищує результати базових
моделей на 15–20%, а можливість інтерпретованого сценарного аналізу робить метод
придатним для практичного використання в агросекторі. Запропонований підхід може
60
бути використаний для оцінки виробничих ризиків, оперативного моніторингу стану
посівів та підтримки управлінських рішень, що потенційно дозволяє зменшити
економічні втрати за сезон. У наступних підпунктах розглянуто наявні обмеження та
перспективи подальшого розвитку системи.
4.2 Практична цінність
Запропонований підхід MTCR-CS має помітну практичну цінність, оскільки
поєднує дві важливі властивості: ефективне інтелектуальне узагальнення
гетерогенних агроданих у спільному латентному просторі та можливість проведення
сценарного аналізу з урахуванням метеоумов. Це дає змогу перейти від реактивного
моніторингу до проактивного управління агровиробництвом — тобто приймати
рішення на основі прогнозних оцінок і «що-якщо» сценаріїв. У цій частині викладено
ключові напрями практичного застосування, оцінено потенційну економічну віддачу
та наведено зауваження щодо інтеграції з існуючими інформаційними системами.
По-перше, для індивідуальних господарств і агрохолдингів MTCR-CS може
стати інструментом раннього виявлення кліматичних ризиків і оцінки їхнього впливу
на індекси вегетації. У нашому випадку в якості цільової величини використано VARI
як найбільш наближену до NDVI міру, доступну при наявності трьох спектральних
каналів. Навчена прогнозуюча голова, що опирається на фіксовані латентні
представлення MTCR, демонструє достатній рівень точності на валідаційній вибірці
(див. розділ 3): середні значення MSE, RMSE, MAE та коефіцієнта детермінації
дозволяють відтворювати сезонні тенденції VARI і виявляти значні відхилення в
умовах сильних погодних змін. На практиці це означає, що фермер, маючи можливість
ввести припущений сценарій зміни погодних показників (наприклад, зменшення
опадів у конкретному місяці), може отримати кількісну оцінку очікуваної зміни
індексу вегетації та, відповідно, приблизної втрати продуктивності, що дає підстави
для коригування агротехніки (зрошення, норма добрив, захисні заходи) на етапі
61
планування. Така операційна корекція дозволяє мінімізувати втрати продукції та
зменшити неоптимальні витрати ресурсів.
По-друге, архітектура моделі спроектована з урахуванням інтеграції в існуючі
інструменти обробки і візуалізації. Результуючі латентні вектори та прогнози легко
експортуються у форматах, сумісних з ГІС (наприклад, GeoTIFF або Shapefile), що
забезпечує їхнє безшовне відображення на інтерактивних картах. На базі відкритих
фреймворків для веб-розробки (наприклад, Streamlit або Flask з Leaflet/Folium) можна
реалізувати веб-інтерфейс, у якому користувачі вводять сценарні метеопрогнози та
отримують просторові теплові карти ризиків і табличні звіти з очікуваними змінами
VARI. Така інтеграція робить систему доступною для агронома, менеджера
агрохолдингу чи консультанта і полегшує масштабування рішення на тисячі полів.
Окрім візуалізації, сервіс може працювати як API-компонент у ланцюгу автоматичного
оновлення даних (наприклад, з Open-Meteo або іншого джерела), що дозволить
отримувати оперативні оцінки ризиків у режимі, наближеному до реального часу.
По-третє, MTCR-CS має економічний потенціал, який випливає з можливостей
сценарного планування. Використання моделі для своєчасного коригування
агротехнологій дозволяє зменшити ризики недоотримання врожаю. Оцінки
економічної ефективності залежать від локальних умов, цін на культуру та
інтенсивності виробництва; попередні розрахунки і прикладні сценарії (наведені у
розділі 3) демонструють, що коригувальні заходи, ініційовані за результатами
сценарного аналізу, можуть компенсувати частину втрат, пов’язаних із
несприятливими погодними подіями. Сам по собі MTCR-CS не є автоматичним
засобом підвищення врожайності — його роль у тому, щоб надати кількісні оцінки і
сценарні прогнози, на основі яких менеджмент приймає обґрунтовані рішення. З
огляду на це, економічна вигода від впровадження буде пропорційна сутності реакцій
на отримані прогнози (зрошення, внесення добрив, зміни сівозмін тощо) і вартості цих
заходів.
62
Крім того, запропонований підхід добре узгоджується з практикою застосування
ГІС-інструментів у агрономії. Експорт результатів як шарів з атрибутами прогнозу і
чутливості дозволяє інтегрувати MTCR-CS у робочі процеси агрономічних служб та
систем управління господарством, що сприяє оперативному плануванню і
відстеженню стану посівів. Така сумісність із існуючими системами (локальними та
хмарними) підвищує ймовірність швидкого впровадження і зменшує початкові
витрати на інтеграцію.
Окремо слід відзначити універсальність методу: підхід на базі контрастивного
самонавчання і прогнозної голови можна адаптувати до інших галузей, де є поєднання
зображень, часових рядів і табличних даних. У медицині це може бути прогнозування
клінічних показників з використанням зображень і електрофізіологічних сигналів; в
екології — прогноз змін покриву або водного режиму з використанням супутникових
даних і наземних спостережень; у страховій і фінансовій аналітиці — сценарний
аналіз впливу макро-факторів на ризики. Застосування в інших доменах потребує
адаптації предобробки і підбору релевантних ознак, але загальна архітектура
збережеться, що скорочує час і вартість розробки нових рішень.
На завершення слід підкреслити, що практична цінність MTCR-CS полягає не
лише у можливості отримувати більш точні просторово-часові прогнози індексів
вегетації, але й у створенні дескриптивного та сценарного інструментарію для
прийняття рішень. Архітектура дозволяє гнучко масштабувати рішення, інтегрувати
його в існуючі робочі процеси агровиробництва та використовувати для створення
сервісів із прикладною цінністю для фермерів, агрохолдингів та консалтингових
компаній. Для повного переходу до промислового впровадження рекомендується
виконати пілотні проєкти в регіонах з різними типами ґрунтів і культур, уточнити
економічні моделі і провести додаткову валідацію прогнозів на незалежних вибірках.
63
4.3 Обмеження запропонованого підходу
Запропонований підхід MTCR-CS має кілька важливих обмежень, про які слід
відкрито говорити, оскільки вони визначають межі застосовності моделі та напрямки
подальшого вдосконалення. По-перше, обмеження пов'язані з даними. Дослідження
базується на трирічному масиві спостережень, який охоплює лише один регіон. Така
вибірка добре відображає локальні особливості Черкаської області, але не гарантує
коректної роботи в інших природних зонах з відмінним кліматом або аграрними
практиками. Крім того, погодні дані зіставляються по центроїдам громад, а це означає,
що всі поля в межах однієї громади отримують однаковий погодний сигнал. Для
великих громад це призводить до систематичних похибок, бо мікроклімат всередині
громади може суттєво відрізнятися. Нарешті, спектральний склад наявних зображень
обмежений трьома каналами, тому для індексів, які вимагають ближнього
інфрачервоного діапазону, доводиться використовувати апроксимації. Апроксимація
дає корисну інформацію, але її точність нижча, ніж при використанні класичного
індексу NDVI на основі NIR.
По-друге, є архітектурні обмеження. Механізм, що використовується для оцінки
чутливості моделі до змін погодних факторів, дає інформативні сигнали, але він має
кореляційний характер. Це означає, що оцінки впливу погодних змін вказують на
асоціації, а не на строгі причинні взаємозв'язки. У присутності прихованих факторів
або коли різні чинники взаємодіють складнішим чином, інтерпретація результатів
може ввести в оману. Крім того, контрастивне самонавчання сильно залежить від
якості аугментацій та варіацій у даних. Якщо аугментації занадто слабкі, латентний
простір може не бути стійким до реальних змін умов зйомки, наприклад змін
хмарності чи освітленості, і це позначається на кінцевій точності прогнозу.
По-третє, є практичні обмеження, пов'язані з обчислювальними ресурсами та
масштабуванням. Попереднє навчання моделі потребує сучасного графічного
процесора і значного часу на обчислення, що ускладнює розгортання підходу на
64
підприємствах без доступу до відповідної інфраструктури. Також в рамках роботи не
було виконано повного пошуку гіперпараметрів і повного тестування на різних
культурах і регіонах, тому нинішні результати слід вважати попередніми. Без цих
додаткових експериментів не можна гарантувати, що модель збереже свої властивості
при істотній зміні вхідних даних або при переході на інший спектральний набір.
Незважаючи на перелічені обмеження, вони не заперечують корисності підходу.
Навпаки, вони визначають конкретні напрями роботи для майбутніх досліджень.
Серед пріоритетних кроків слід відзначити розширення навчальної вибірки в просторі
і в часі, використання більш детальної просторової погодної інформації замість одного
центроїда на громаду, отримання або додавання каналів ближнього інфрачервоного
діапазону для покращення індексів вегетації, посилення аугментацій і проведення
більш ретельного пошуку гіперпараметрів, а також дослідження підходів, які
дозволяють краще підходити до причинної інтерпретації результатів. Також варто
розглянути інструменти для зменшення вимог до обладнання, такі як квантовація
мереж, знання-виключення і дистиляція моделей, що полегшить практичне
впровадження на місцях.
Підсумовуючи, обмеження MTCR-CS стосуються даних, архітектури та
ресурсів. Вони служать корисною картою проблем, які потрібно вирішити для
переходу від дослідницького прототипу до надійного прикладного інструменту.
Пріоритетні напрямки удосконалення включають розширення і різноманіття даних,
посилення методів просторового прив’язування погодних даних, покращення
спектрального покриття та роботу над інтерпретованістю результатів. Імплементація
цих кроків дозволить зменшити невизначеність і підвищити практичну надійність
підходу.
65
4.4 Перспективи подальших досліджень
Подальший розвиток методу MTCR-CS може бути спрямований на поступове
розширення його функціональних можливостей та усунення обмежень, визначених у
попередньому підпункті. Це відкриває перспективи для більш широкого застосування
підходу як у сільському господарстві, так і в інших сферах, де широко
використовуються мультимодальні дані.
Одним із ключових напрямів удосконалення є розширення типів даних, які може
обробляти система. На сьогодні модель працює переважно з супутниковими знімками
та метеорологічною інформацією, але включення додаткових джерел, таких як дані від
ґрунтових та польових сенсорів, показники вологості ґрунту або інформація про
проведені агротехнічні роботи, дозволило б сформувати повніший опис стану посівів.
Це може суттєво підвищити точність прогнозів, однак вимагатиме адаптації
інструментарію для роботи з новими типами сигналів та збільшення обсягів
навчальних вибірок.
Перспективним напрямом є також удосконалення інструментів аналізу
причинно-наслідкових зв’язків. У поточній реалізації оцінка чутливості базується на
штучних змінах у вхідних даних, що не завжди дозволяє чітко розмежувати реальні
причинні ефекти та звичайні статистичні залежності. Використання підходів,
орієнтованих на побудову структур причинності, могло б забезпечити більш коректне
трактування впливу окремих факторів, що має важливе значення для практичних
рішень в агросекторі.
Окрему увагу слід приділити масштабуванню моделі на інші регіони та
культури. Оскільки сучасна версія базується лише на даних Черкаської області, її
перенесення на території з іншими кліматичними умовами потребуватиме додаткової
перевірки. Аналогічним чином, застосування до інших культур вимагатиме
розширення набору даних та аналізу загальної здатності моделі узагальнювати
інформацію у більш різноманітних умовах. Це створює підґрунтя для формування
66
єдиної національної системи моніторингу, здатної охоплювати більшу кількість
регіонів і культур.
Подальший розвиток методу може також передбачати покращення можливостей
роботи в режимі близькому до реального часу. Перехід від місячної періодичності до
щоденних або тижневих оновлень забезпечив би більш оперативне реагування на
зміни умов вирощування. Це, у свою чергу, створює передумови для появи зручних
мобільних рішень, які могли б стати доступним інструментом для фермерів на
щоденній основі.
Окремою перспективою є інтеграція результатів MTCR-CS із довгостроковими
кліматичними прогнозами. Поєднання короткострокових оцінок стану посівів із
багаторічними кліматичними сценаріями дозволило б підтримувати стратегічне
планування в аграрній сфері, зокрема в питаннях визначення структури посівів,
адаптації до змін клімату та прогнозування потенційних ризиків.
У сукупності перелічені напрями розвитку створюють основу для подальшого
вдосконалення методу та розширення сфер його застосування. Це дозволить
збільшити точність прогнозів, підвищити надійність висновків та зробити систему
більш придатною для використання у реальних виробничих умовах.
4.5 Висновки до 4 розділу
Отримані результати дають підстави зробити узагальнений висновок щодо
ефективності використання підходу MTCR-CS у рамках проведеного дослідження. На
основі численних експериментів видно, що модель здатна узгоджувати різні типи
аграрних даних у спільному просторі представлень та забезпечувати достатню якість
прогнозування вегетаційних показників. Сформовані латентні ознаки виявилися
інформативними для подальших задач, а поведінка моделі при моделюванні різних
погодних сценаріїв підтвердила її здатність реагувати на ключові зміни у кліматичних
умовах. Такі результати свідчать про те, що обраний підхід може бути корисним не
67
лише для аналізу історичних даних, але й для розв’язання більш прикладних задач,
пов’язаних з оцінкою ризиків та підтримкою управлінських рішень у агровиробництві.
У сукупності це демонструє перспективність застосування MTCR-CS та формує
основу для подальшого розвитку системи, включно з розширенням набору даних,
адаптацією до інших регіонів та культур і вдосконаленням методів інтерпретації
результатів.
68
ВИСНОВКИ
У ході виконання кваліфікаційної роботи було розроблено, реалізовано та
ґрунтовно досліджено мультимодальний часовий контрастивний підхід з механізмом
причинної чутливості MTCR-CS, призначений для аналізу сільськогосподарських
даних. Запропонований метод повною мірою забезпечив досягнення поставленої
мети: сформовано єдиний латентний простір для представлення гетерогенних даних,
до яких належать супутникові знімки, метеорологічні характеристики та властивості
ґрунтів. Створена модель забезпечує прогнозування індексу вегетації на місяць уперед
та дає змогу виконувати інтерпретований сценарний аналіз впливу кліматичних
чинників без повторного навчання.
Експериментальні дослідження, проведені на реальному наборі даних
Черкаської області обсягом близько ста дев'яноста восьми тисяч полів за період з 2021
до 2023 рр., підтвердили високу результативність MTCR-CS. Механізм причинної
чутливості достовірно відтворює очікувані агрономічні закономірності, що створює
можливість кількісного оцінювання ризиків і планування захисних заходів на основі
даних.
Розроблений підхід має значну практичну цінність, оскільки може бути
інтегрований у наявні геоінформаційні системи, підтримує експорт результатів у
поширені геопросторові формати та відкриває перспективи створення вебсервісів і
мобільних рішень для інтерактивного моніторингу. Метод легко адаптується до інших
регіонів України та різних сільськогосподарських культур, що робить його придатним
для переходу від реактивного до проактивного управління кліматичними ризиками й
потенційно сприяє зменшенню економічних втрат агровиробників.
Разом із тим визначено низку обмежень, що пов’язані з локальністю
використаного датасету, застосуванням апроксимації VARI замість NDVI,
кореляційною природою механізму чутливості та значними обчислювальними
69
потребами. Ці обмеження не знижують цінності отриманих результатів, а задають
чіткі напрями подальших досліджень і вдосконалення підходу.
У підсумку MTCR-CS постає як науково обґрунтований, технічно реалізований
і практично значущий інструмент аналізу мультимодальних даних у сфері сільського
господарства. Запропонований метод формує стійку основу для подальшого розвитку
систем підтримки прийняття рішень, сприяє підвищенню адаптивності аграрного
виробництва України в умовах кліматичної невизначеності та має перспективи
масштабування на національному й міжнародному рівнях.
70
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ
1. Міністерство аграрної політики та продовольства України. (2023). Головна |
Міністерство аграрної політики та продовольства України.
https://minagro.gov.ua/
2. Food and Agriculture Organization of the United Nations. (2025). ООН
прогнозирует рост производства продовольствия на 50–70% к 2050 году.
https://www.mk.ru/economics/2025/11/29/oon-prognoziruet-rost-proizvodstva-
prodovolstviya-na-5070-k-2050-godu.html
3. Schmitt, M., Hughes, L. H., Qiu, C., & Zhu, X. X. (2019). SEN12MS – A Curated
Dataset of Georeferenced Multi-spectral Sentinel-1/2 Imagery for Deep Learning and
Data Fusion. arXiv. https://arxiv.org/abs/1906.07789
4. Sumbul, G., Charfuelan, M., Demir, B., & Markl, V. (2019). BigEarthNet: A Large-
Scale Benchmark Archive for Remote Sensing Image Understanding. IEEE
International Geoscience and Remote Sensing Symposium. https://bigearth.net/
5. Wang, Y., & Bai, X. (2020). Multilevel data fusion for the internet of things in smart
agriculture. Computers and Electronics in Agriculture, 171, 105309.
https://doi.org/10.1016/j.compag.2020.105309
6. Sadeghi-Tehran, P., Virlet, N., Sabermanesh, K., & Hawkesford, M. J. (2017). Multi-
feature machine learning model for automatic segmentation of green fractional
vegetation cover for high-throughput field phenotyping. Frontiers in Plant Science, 8,
2046. https://doi.org/10.3389/fpls.2017.02046
7. Van Klompenburg, T., Kassahun, A., & Catal, C. (2020). Crop yield prediction using
machine learning: A systematic literature review. Computers and Electronics in
Agriculture, 177, 105709. https://doi.org/10.1016/j.compag.2020.105709
8. Bolton, D. K., & Friedl, M. A. (2013). Forecasting crop yield using remotely sensed
vegetation indices and crop phenology metrics. Agricultural and Forest Meteorology,
173, 74–84. https://doi.org/10.1016/j.agrformet.2013.01.007
9. Bolton, D. K., & Friedl, M. A. (2020). Forecasting crop yield using remotely sensed
vegetation indices and crop phenology metrics. Agricultural and Forest Meteorology,
173, 74–84. https://doi.org/10.1016/j.agrformet.2013.01.007
10. Cai, Y., Guan, K., Lobell, D., Potgieter, A. B., Wang, S., Peng, J., Xu, T., Asseng, S.,
Zhang, Y., You, L., & Peng, B. (2019). Integrating satellite and climate data to predict
wheat yield in Australia using machine learning approaches. Agricultural and Forest
Meteorology, 274, 144–159. https://doi.org/10.1016/j.agrformet.2019.03.001
71
11. Schmitt, M., Hughes, L. H., Qiu, C., & Zhu, X. X. (2019). SEN12MS – A Curated
Dataset of Georeferenced Multi-spectral Sentinel-1/2 Imagery for Deep Learning and
Data Fusion. arXiv. https://arxiv.org/abs/1906.07789
12. Weiss, M., Jacob, F., & Duveiller, G. (2020). Remote sensing for agricultural
applications: A meta-review. Remote Sensing of Environment, 236, 111402.
https://doi.org/10.1016/j.rse.2019.111402
13. Wang, S., Chen, J., Chen, K., Wang, X., & Navlakha, S. (2021). Spatial-temporal crop
yield prediction via CNN on multi-source satellite data. Remote Sensing, 13(14),
2823. https://doi.org/10.3390/rs13142823
14. Khaki, S., Wang, L., & Archontoulis, S. V. (2020). A CNN-RNN framework for crop
yield prediction. Frontiers in Plant Science, 10, 1750.
https://doi.org/10.3389/fpls.2019.01750
15. You, J., Li, X., Low, M., Lobell, D., & Ermon, S. (2021). Deep Gaussian process for
crop yield prediction based on remote sensing data. Proceedings of the AAAI
Conference on Artificial Intelligence, 31(1), 4559–4566.
https://ojs.aaai.org/index.php/AAAI/article/view/17133
16. Zhang, H., Yuan, Y., Lin, Z., Ji, Q., & Lu, H. (2023). Boosting semi-supervised
semantic segmentation with probabilistic representations. Proceedings of the AAAI
Conference on Artificial Intelligence, 37(3), 3563–3571.
https://ojs.aaai.org/index.php/AAAI/article/view/25539
17. Brodersen, K. H., Gallusser, F., Koehler, J., Remy, N., & Scott, S. L. (2015). Inferring
causal impact using Bayesian structural time-series models. Annals of Applied
Statistics, 9(1), 247–274. https://doi.org/10.1214/14-AOAS788
18. Van Klompenburg, T., Kassahun, A., & Catal, C. (2021). Crop yield prediction using
machine learning: A systematic literature review. Computers and Electronics in
Agriculture, 177, 105709. https://doi.org/10.1016/j.compag.2020.105709
19. Sharma, A., Jain, A., Gupta, P., & Chowdary, V. (2021). Machine learning applications
for precision agriculture: A comprehensive review. IEEE Access, 9, 4843–4873.
https://doi.org/10.1109/ACCESS.2020.3048415
20. Li, S., Dragicevic, S., Castro, F. A., Sester, M., Winter, S., Coltekin, A., Pettit, C.,
Klippel, A., Stein, M., Cheng, X., Kraak, M.-J., Kveladze, I., Manrique-Sancho, M.-
T., & Zurbaran, M. A. (2020). Geospatial big data handling theory and methods: A
review and research challenges. ISPRS Journal of Photogrammetry and Remote
Sensing, 169, 119–134. https://doi.org/10.1016/j.isprsjprs.2020.08.021
21. Chernozhukov, V., Chetverikov, D., Demirer, M., Duflo, E., Hansen, C., Newey, W.,
& Robins, J. (2018). Double/debiased machine learning for treatment and structural
72
parameters. The Econometrics Journal, 21(1), C1–C68.
https://doi.org/10.1111/ectj.12097
22. Schluter, A., Brunner, C., Punzi, M. T., & van Klompenburg, T. (2022). Genetic
algorithms for feature selection when classifying severe chronic disorders of
consciousness. PLOS ONE, 17(7), e0261138.
https://doi.org/10.1371/journal.pone.0261138
23. Russakovsky, O., Deng, J., Su, H., Krause, J., Satheesh, S., Ma, S., Huang, Z.,
Karpathy, A., Khosla, A., Bernstein, M., Berg, A. C., & Fei-Fei, L. (2015). ImageNet
large scale visual recognition challenge. International Journal of Computer Vision,
115(3), 211–252. https://doi.org/10.1007/s11263-015-0816-y
73
ДОДАТОК А
Затверджую»
Завідувач кафедри статистики
та прикладної математики
____________ Анаіт КАРАПЕТЯН
«___» ________2025 р.
ПРОГНОЗУВАННЯ ВРОЖАЙНОСТІ СІЛЬСЬКОГОСПОДАРСЬКИХ
КУЛЬТУР ІЗ ВИКОРИСТАННЯМ МЕТОДІВ
ГЛИБИННОГО НАВЧАННЯ
Специфікація
482.ЧДТУ.5 2434-01
Листів 2
Розробник Назар КОЗАК
Керівник Анаіт КАРАПЕТЯН
74
Черкаси 2025
75
482.ЧДТУ.5 2434-01
Позначення Найменування Примітка
Документація
482.ЧДТУ.5 2114-01 12 01 Текст програми
76
ДОДАТОК Б
ПРОГНОЗУВАННЯ ВРОЖАЙНОСТІ СІЛЬСЬКОГОСПОДАРСЬКИХ
КУЛЬТУР ІЗ ВИКОРИСТАННЯМ МЕТОДІВ ГЛИБИННОГО
НАВЧАННЯ
Текст програми
482.ЧДТУ. 2434-01 12 01
Листів 68
Розробник Назар КОЗАК
Черкаси 2025
77
# save_fields_geometries.ipynb
import os
os.environ["PROJ_LIB"] =
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\.venv\Lib\site-
packages\pyproj\proj_dir\share\proj"
os.environ["GDAL_DATA"] =
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\.venv\Lib\site-
packages\rasterio\gdal_data"
import warnings
import logging
from pathlib import Path
import geopandas as gpd
import rasterio
from rasterio.mask import mask
from shapely.geometry import mapping
import pandas as pd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s:
%(message)s")
FIELD_BOUNDARIES_TEMPLATE =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\cherkasy_field_boundari
es_{}\Cherkasy_Field_Boundaries_{}.shp"
TIFF_IMAGE_TEMPLATE =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\{}\sen
tinel_data\Cherkassy_{}_{:02d}_RGB_Median.tif"
RESULT_PATH =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\images\{}"
COMMUNITY_BOUNDARIES =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\reprojected_communities
\cherkassy_communities.shp"
MANIFEST_PATH =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\manife
st_fields.csv" # вихідна таблиця
YEARS = [2021, 2022, 2023]
MONTHS = list(range(1, 13))
TARGET_EPSG = 32636 # EPSG:32636
MIN_VALID_PIXELS = 10
def ensure_dir(path):
78
Path(path).mkdir(parents=True, exist_ok=True)
logging.info("Loading community boundaries ...")
communities = gpd.read_file(COMMUNITY_BOUNDARIES)
if communities.crs is None:
raise RuntimeError(f"Community shapefile {COMMUNITY_BOUNDARIES} has no
CRS — вкажіть CRS або ре-проєктуйте файл.")
if communities.crs.to_epsg() != TARGET_EPSG:
logging.info(f"Reprojecting communities from {communities.crs} to
EPSG:{TARGET_EPSG}")
communities = communities.to_crs(epsg=TARGET_EPSG)
if "koatuu" not in communities.columns and "KOATUU" in communities.columns:
communities = communities.rename(columns={"KOATUU": "koatuu"})
if "koatuu" not in communities.columns:
logging.warning(
"Колонка 'koatuu' не знайдена у шарі громад. Задайте правильну назву
або додайте колонку 'koatuu' у shapefile.")
manifest_rows = []
errors = []
for year in YEARS:
logging.info(f"Processing year {year} ...")
field_shp_path = FIELD_BOUNDARIES_TEMPLATE.format(year, year)
if not os.path.exists(field_shp_path):
logging.error(f"Field boundaries for year {year} not found:
{field_shp_path}")
continue
logging.info(f"Reading field boundaries: {field_shp_path}")
fields = gpd.read_file(field_shp_path)
if fields.crs is None:
warnings.warn(f"Field shapefile {field_shp_path} has no CRS —
припускаю, що потрібно задати його вручну.")
if fields.crs is None or fields.crs.to_epsg() != TARGET_EPSG:
logging.info(f"Reprojecting field boundaries to EPSG:{TARGET_EPSG}
...")
try:
fields = fields.to_crs(epsg=TARGET_EPSG)
except Exception as e:
logging.error(f"Помилка репроєкції для {field_shp_path}: {e}")
errors.append(("reproject_fields", field_shp_path, str(e)))
continue
id_col = "Field_ID"
if id_col is None:
79
logging.warning("Не знайдено очевидної колонки з field_id — буде
використаний індекс GeoDataFrame (gdf.index).")
fields = fields.reset_index().rename(columns={"index":
"field_index"})
id_col = "field_index"
logging.info("Performing spatial join fields -> communities to get koatuu
...")
try:
joined = gpd.sjoin(fields, communities[["koatuu", "geometry"]],
how="left", predicate="within")
except Exception as e:
logging.warning(f"sjoin with predicate='within' failed: {e}. Trying
intersect.")
joined = gpd.sjoin(fields, communities[["koatuu", "geometry"]],
how="left", predicate="intersects")
missing_ko = joined["koatuu"].isna()
if missing_ko.any():
logging.info(
f"{missing_ko.sum()} fields have no containing community;
assigning nearest community centroid ...")
comm_centroids = communities.copy()
comm_centroids["geometry"] = comm_centroids.geometry.centroid
comm_centroids =
comm_centroids.reset_index().rename(columns={"index": "comm_idx"})
for idx in joined[missing_ko].index:
field_geom = joined.at[idx, "geometry"]
try:
distances = comm_centroids.geometry.distance(field_geom)
nearest_idx = distances.idxmin()
joined.at[idx, "koatuu"] = comm_centroids.at[nearest_idx,
"koatuu"]
except Exception as e:
errors.append(("nearest_assign", joined.at[idx, id_col],
str(e)))
joined.at[idx, "koatuu"] = None
year_folder = RESULT_PATH.format(year)
for month in MONTHS:
tiff_image_name = TIFF_IMAGE_TEMPLATE.format(year, year, month)
if not os.path.exists(tiff_image_name):
logging.error(f"Monthly tiff not found: {tiff_image_name} —
пропускаю місяць {month} року {year}.")
errors.append(("missing_tiff", year, month, tiff_image_name))
continue
month_folder = os.path.join(year_folder, f"{month:02d}")
80
ensure_dir(month_folder)
with rasterio.open(tiff_image_name) as src:
raster_crs = src.crs
if raster_crs is not None and raster_crs.to_epsg() !=
TARGET_EPSG:
logging.info(
f"Raster CRS EPSG:{raster_crs.to_epsg()} відрізняється
від TARGET EPSG:{TARGET_EPSG}; репроєктую поля у CRS растра для маскування.")
fields_for_mask = joined.to_crs(raster_crs)
else:
fields_for_mask = joined
for i, row in fields_for_mask.iterrows():
field_id = row[id_col]
koatuu = row.get("koatuu", None)
sample_fname = f"FIELD_{field_id}_{month:02d}_{year}.tif"
out_path = os.path.join(month_folder, sample_fname)
if os.path.exists(out_path):
logging.debug(f"Exists, skip: {out_path}")
manifest_rows.append({
"field_id": field_id,
"koatuu": koatuu,
"month": month,
"year": year,
"image_path": out_path,
"valid": True
})
continue
geom = row.geometry
if geom is None or geom.is_empty:
logging.warning(f"Empty geometry for field {field_id}
(year {year}) — пропускаю.")
errors.append(("empty_geom", field_id, year))
manifest_rows.append({
"field_id": field_id,
"koatuu": koatuu,
"month": month,
"year": year,
"image_path": None,
"valid": False
})
continue
try:
geom_json = [mapping(geom)]
81
out_image, out_transform = mask(src, geom_json,
crop=True, all_touched=False, nodata=src.nodata)
if src.nodata is not None:
valid_mask = (out_image != src.nodata).any(axis=0)
else:
valid_mask = ~((out_image == 0).all(axis=0))
valid_pixels = int(valid_mask.sum())
if valid_pixels < MIN_VALID_PIXELS:
logging.info(
f"Field {field_id} month {month} year {year}:
only {valid_pixels} valid pixels -> skip saving.")
manifest_rows.append({
"field_id": field_id,
"koatuu": koatuu,
"month": month,
"year": year,
"image_path": None,
"valid": False
})
continue
out_meta = src.meta.copy()
out_meta.update({
"driver": "GTiff",
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform,
})
with rasterio.open(out_path, "w", **out_meta) as dst:
dst.write(out_image)
manifest_rows.append({
"field_id": field_id,
"koatuu": koatuu,
"month": month,
"year": year,
"image_path": out_path,
"valid": True
})
except Exception as e:
logging.error(f"Error clipping field {field_id} (year
{year}, month {month}): {e}")
errors.append(("clip_error", field_id, year, month,
str(e)))
manifest_rows.append({
"field_id": field_id,
82
"koatuu": koatuu,
"month": month,
"year": year,
"image_path": None,
"valid": False
})
logging.info("Saving manifest ...")
manifest_df = pd.DataFrame(manifest_rows, columns=["field_id", "koatuu",
"month", "year", "image_path", "valid"])
ensure_dir(os.path.dirname(MANIFEST_PATH))
manifest_df.to_csv(MANIFEST_PATH, index=False)
parquet_path = os.path.splitext(MANIFEST_PATH)[0] + ".parquet"
manifest_df.to_parquet(parquet_path, index=False)
logging.info(f"Manifest saved to {MANIFEST_PATH} and {parquet_path}")
if errors:
err_df = pd.DataFrame(errors, columns=["error_type", "id_or_path",
"extra", "more"])
err_path = os.path.splitext(MANIFEST_PATH)[0] + "_errors.csv"
err_df.to_csv(err_path, index=False)
logging.warning(f"Encountered {len(errors)} errors. See {err_path}")
logging.info("Processing finished.")
# sentinel_images.ipynb
import rasterio
from rasterio.mask import mask
import geopandas as gpd
import os
from shapely.geometry import mapping
# Шляхи до файлів
raster_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\s
entinel_data\Cherkassy_2021_05_RGB_Median.tif"
vector_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\s
hp fields\fields2021.shp"
output_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\c
lipped\clipped_image.tif"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
83
vector = gpd.read_file(vector_path)
with rasterio.open(raster_path) as src:
raster_crs = src.crs
print("CRS растра:", raster_crs)
print("Межі растра:", src.bounds)
if vector.crs != raster_crs:
vector = vector.to_crs(raster_crs)
print("Полігон перепроєктовано у CRS растра.")
print("Межі полігону:", vector.total_bounds)
shapes = [mapping(geom) for geom in vector.geometry]
out_image, out_transform = mask(src, shapes, crop=True)
out_meta = src.meta.copy()
out_meta.update({
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform
})
with rasterio.open(output_path, "w", **out_meta) as dest:
dest.write(out_image)
print("Файл збережено тут:", output_path)
from tqdm import tqdm
tiffs_template =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\s
entinel_data\Cherkassy_2023_{}_RGB_Median.tif"
vector_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\s
hp fields\fields2023.shp"
output_template =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\c
lipped\clipped_field_{}_2023.tif"
vector = gpd.read_file(vector_path)
for i in tqdm(range(1, 13)):
month = f"{i:02d}"
84
tiff_path = tiffs_template.format(month)
output_path = output_template.format(month)
with rasterio.open(tiff_path) as src:
raster_crs = src.crs
# Перепроєктуємо полігон у CRS растра
if vector.crs != raster_crs:
vector = vector.to_crs(raster_crs)
# Перетворюємо геометрії у формат, який розуміє rasterio
shapes = [mapping(geom) for geom in vector.geometry]
# Обрізання растрового шару
out_image, out_transform = mask(src, shapes, crop=True)
out_meta = src.meta.copy()
# Оновлюємо метаінформацію під обрізаний растер
out_meta.update({
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform
})
# Записуємо результат
with rasterio.open(output_path, "w", **out_meta) as dest:
dest.write(out_image)
import rasterio
tif_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\images\2021\01\FIELD_19_01_2021.tif"
with rasterio.open(tif_path) as src:
print("Кількість каналів (бендів):", src.count)
print("Імена каналів / індексація:", src.indexes)
print("Розмір зображення (height, width):", src.height, src.width)
# data_preparation.ipynb
85
import os
os.environ["OGR_GEOJSON_MAX_OBJ_SIZE"] = "0"
import geopandas as gpd
DATA_FOLDER =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields"
#DISSOLVED_2021_PATH = os.path.join(DATA_FOLDER, "2021",
"dissolved_2021.geojson")
#DISSOLVED_2022_PATH = os.path.join(DATA_FOLDER, "2022",
"dissolved_2022.geojson")
#DISSOLVED_2023_PATH = os.path.join(DATA_FOLDER, "2023",
"dissolved_2023.geojson")
CENTROIDS_2021_PATH = os.path.join(DATA_FOLDER, "2021",
"centroids_2021.geojson")
CENTROIDS_2022_PATH = os.path.join(DATA_FOLDER, "2022",
"centroids_2022.geojson")
CENTROIDS_2023_PATH = os.path.join(DATA_FOLDER, "2023",
"centroids_2023.geojson")
#DISSOLVED_2021 = gpd.read_file(DISSOLVED_2021_PATH)
#DISSOLVED_2022 = gpd.read_file(DISSOLVED_2022_PATH)
#DISSOLVED_2023 = gpd.read_file(DISSOLVED_2023_PATH)
CENTROIDS_2021 = gpd.read_file(CENTROIDS_2021_PATH)
CENTROIDS_2022 = gpd.read_file(CENTROIDS_2022_PATH)
CENTROIDS_2023 = gpd.read_file(CENTROIDS_2023_PATH)
centroids = [CENTROIDS_2021, CENTROIDS_2022, CENTROIDS_2023]
for centroid in centroids:
centroid["lon"] = centroid.geometry.x
centroid["lat"] = centroid.geometry.y
CENTROIDS_2021["ID"] = CENTROIDS_2021["koatuu"].astype(str) + "_2021"
CENTROIDS_2022["ID"] = CENTROIDS_2022["koatuu"].astype(str) + "_2022"
CENTROIDS_2023["ID"] = CENTROIDS_2023["koatuu"].astype(str) + "_2023"
import openmeteo_requests
import pandas as pd
import requests_cache
from retry_requests import retry
86
def get_meteo_data(latitude, longitude, year):
cache_session = requests_cache.CachedSession('.cache', expire_after = -1)
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
url = "https://archive-api.open-meteo.com/v1/archive"
params = {
"latitude": latitude,
"longitude": longitude,
"start_date": f"{year}-01-01",
"end_date": f"{year}-12-31",
"daily": ["sunshine_duration", "precipitation_sum", "snowfall_sum",
"rain_sum", "surface_pressure_mean", "relative_humidity_2m_mean",
"cloud_cover_mean", "wind_speed_10m_mean", "soil_temperature_0_to_7cm_mean",
"soil_moisture_0_to_7cm_mean", "temperature_2m_mean"],
"timezone": "auto",
"timeformat": "unixtime",
}
responses = openmeteo.weather_api(url, params=params)
response = responses[0]
#print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E")
#print(f"Elevation: {response.Elevation()} m asl")
#print(f"Timezone:
{response.Timezone()}{response.TimezoneAbbreviation()}")
#print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s")
daily = response.Daily()
daily_sunshine_duration = daily.Variables(0).ValuesAsNumpy()
daily_precipitation_sum = daily.Variables(1).ValuesAsNumpy()
daily_snowfall_sum = daily.Variables(2).ValuesAsNumpy()
daily_rain_sum = daily.Variables(3).ValuesAsNumpy()
daily_surface_pressure_mean = daily.Variables(4).ValuesAsNumpy()
daily_relative_humidity_2m_mean = daily.Variables(5).ValuesAsNumpy()
daily_cloud_cover_mean = daily.Variables(6).ValuesAsNumpy()
daily_wind_speed_10m_mean = daily.Variables(7).ValuesAsNumpy()
daily_soil_temperature_0_to_7cm_mean = daily.Variables(8).ValuesAsNumpy()
daily_soil_moisture_0_to_7cm_mean = daily.Variables(9).ValuesAsNumpy()
daily_temperature_2m_mean = daily.Variables(10).ValuesAsNumpy()
daily_data = {"date": pd.date_range(
start = pd.to_datetime(daily.Time(), unit = "s", utc = True),
end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True),
freq = pd.Timedelta(seconds = daily.Interval()),
inclusive = "left"
)}
87
daily_data["sunshine_duration"] = daily_sunshine_duration
daily_data["precipitation_sum"] = daily_precipitation_sum
daily_data["snowfall_sum"] = daily_snowfall_sum
daily_data["rain_sum"] = daily_rain_sum
daily_data["surface_pressure_mean"] = daily_surface_pressure_mean
daily_data["relative_humidity_2m_mean"] = daily_relative_humidity_2m_mean
daily_data["cloud_cover_mean"] = daily_cloud_cover_mean
daily_data["wind_speed_10m_mean"] = daily_wind_speed_10m_mean
daily_data["soil_temperature_0_to_7cm_mean"] =
daily_soil_temperature_0_to_7cm_mean
daily_data["soil_moisture_0_to_7cm_mean"] =
daily_soil_moisture_0_to_7cm_mean
daily_data["temperature_2m_mean"] = daily_temperature_2m_mean
daily_dataframe = pd.DataFrame(data = daily_data)
return daily_dataframe
import time
from tqdm import tqdm
METEODATA_2021 =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\m
eteo_data"
METEODATA_2022 =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2022\m
eteo_data"
METEODATA_2023 =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\m
eteo_data"
os.makedirs(METEODATA_2021, exist_ok=True)
os.makedirs(METEODATA_2022, exist_ok=True)
os.makedirs(METEODATA_2023, exist_ok=True)
CENTROIDS_DATA = [
#(CENTROIDS_2021, 2021, METEODATA_2021),
(CENTROIDS_2022, 2022, METEODATA_2022),
(CENTROIDS_2023, 2023, METEODATA_2023)
]
failed_requests = []
for centroid, year, saving_folder in tqdm(CENTROIDS_DATA, desc="Years"):
for idx, row in tqdm(centroid.iterrows(), total=len(centroid),
88
desc=f"Communities", leave=False):
try:
df = get_meteo_data(
latitude=row["lat"],
longitude=row["lon"],
year=year
)
time.sleep(26)
filename = row["ID"] + ".csv"
filepath = os.path.join(saving_folder, filename)
df.to_csv(filepath, index=False)
if len(df) == 0:
raise ValueError(f"Data for {row['ID']} not found")
except Exception as e:
print(e)
failed_requests.append(row['ID'])
import os
import math
import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString
from itertools import combinations
import matplotlib.pyplot as plt
from scipy.stats import pearsonr, ttest_rel, wilcoxon
COMMUNITIES_SHP = r"C:\Users\nazar\Desktop\projects\masters_thesis\data\межі
громад\Громади_4326.shp" # ← підстав свій shapefile громад
OUT_DIR = "weather_check_results"
YEARS = [2021, 2022, 2023]
os.makedirs(OUT_DIR, exist_ok=True)
gdf = gpd.read_file(COMMUNITIES_SHP)
if gdf.crs is None or gdf.crs.is_geographic:
try:
gdf_proj = gdf.to_crs(epsg=32636)
except Exception:
gdf_proj = gdf.copy()
gdf_proj["geometry"] = gdf_proj.geometry
else:
89
gdf_proj = gdf
gdf_proj["area_m2"] = gdf_proj.geometry.area
largest = gdf_proj.sort_values("area_m2", ascending=False).iloc[0]
largest_id = largest.get("name", largest.get("id", "largest"))
print("Selected community:", largest_id)
poly = largest.geometry
poly_wgs = gpd.GeoSeries([poly], crs=gdf_proj.crs).to_crs(epsg=4326).iloc[0]
def densify_line(coords, step_m=50):
return coords
boundary_coords = list(poly_wgs.exterior.coords)
pts = [(c[0], c[1]) for c in boundary_coords]
def haversine(lon1, lat1, lon2, lat2):
R = 6371000.0
phi1 = math.radians(lat1); phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1); dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi/2.0)**2 +
math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2.0)**2
return 2*R*math.asin(math.sqrt(a))
maxd = -1
pair = None
for (lon1, lat1), (lon2, lat2) in combinations(pts, 2):
d = haversine(lon1, lat1, lon2, lat2)
if d > maxd:
maxd = d
pair = ((lon1, lat1), (lon2, lat2))
print("Farthest pair distance (m):", maxd)
ptA, ptB = pair
with open(os.path.join(OUT_DIR, "selected_points.txt"), "w") as f:
f.write(f"pointA: {ptA}\npointB: {ptB}\ndistance_m: {maxd}\n")
def fetch_years_for_point(lat, lon, years):
frames = []
for y in years:
print(f"Fetching {y} for {lat:.5f},{lon:.5f}")
df = get_meteo_data(lat, lon, y)
df["year"] = y
frames.append(df)
90
if frames:
return pd.concat(frames, ignore_index=True)
return pd.DataFrame()
dfA = fetch_years_for_point(ptA[1], ptA[0], YEARS)
dfB = fetch_years_for_point(ptB[1], ptB[0], YEARS)
dfA.to_csv(os.path.join(OUT_DIR, "pointA_daily.csv"), index=False)
dfB.to_csv(os.path.join(OUT_DIR, "pointB_daily.csv"), index=False)
def monthly_aggregate(df_daily):
df = df_daily.copy()
if "date" in df.columns:
df["date"] = pd.to_datetime(df["date"])
df["month"] = df["date"].dt.month
df["year"] = df["date"].dt.year
else:
raise ValueError("daily dataframe must contain 'date' column")
group = df.groupby(["year","month"]).agg({
"temperature_2m_mean": "mean",
"sunshine_duration": "mean",
"relative_humidity_2m_mean": "mean",
"cloud_cover_mean": "mean",
"surface_pressure_mean": "mean",
"wind_speed_10m_mean": "mean",
"soil_temperature_0_to_7cm_mean": "mean",
"soil_moisture_0_to_7cm_mean": "mean",
"precipitation_sum": "sum",
"rain_sum": "sum",
"snowfall_sum": "sum",
}).reset_index()
return group
monthlyA = monthly_aggregate(dfA)
monthlyB = monthly_aggregate(dfB)
monthlyA.to_csv(os.path.join(OUT_DIR, "pointA_monthly.csv"), index=False)
monthlyB.to_csv(os.path.join(OUT_DIR, "pointB_monthly.csv"), index=False)
def compute_metrics(monthlyA, monthlyB, variables):
m = pd.merge(monthlyA, monthlyB, on=["year","month"],
suffixes=("_A","_B"))
rows = []
for var in variables:
a = m[f"{var}_A"].values
b = m[f"{var}_B"].values
mask = ~np.isnan(a) & ~np.isnan(b)
if mask.sum() == 0:
91
continue
a = a[mask]; b = b[mask]
mae = np.mean(np.abs(a - b))
rmse = np.sqrt(np.mean((a - b)**2))
try:
r, p = pearsonr(a, b)
except Exception:
r, p = np.nan, np.nan
try:
tstat, tp = ttest_rel(a, b)
except Exception:
tstat, tp = np.nan, np.nan
try:
wstat, wp = wilcoxon(a - b)
except Exception:
wstat, wp = np.nan, np.nan
rows.append({
"variable": var,
"n": len(a),
"MAE": mae,
"RMSE": rmse,
"Pearson_r": r,
"Pearson_p": p,
"ttest_stat": tstat,
"ttest_p": tp,
"wilcoxon_stat": wstat,
"wilcoxon_p": wp
})
return pd.DataFrame(rows)
vars_to_check = [
"temperature_2m_mean",
"precipitation_sum",
"rain_sum",
"snowfall_sum",
"sunshine_duration",
"relative_humidity_2m_mean",
"cloud_cover_mean",
"wind_speed_10m_mean",
"soil_temperature_0_to_7cm_mean",
"soil_moisture_0_to_7cm_mean",
]
metrics = compute_metrics(monthlyA, monthlyB, vars_to_check)
metrics.to_csv(os.path.join(OUT_DIR, "comparison_metrics.csv"), index=False)
print(metrics)
92
def plot_time_series_var(monthlyA, monthlyB, var, out_dir):
m = pd.merge(monthlyA, monthlyB, on=["year","month"],
suffixes=("_A","_B"))
if m.empty:
return
# time axis
m["time"] = pd.to_datetime(m["year"].astype(str) + "-" +
m["month"].astype(str) + "-15")
plt.figure(figsize=(10,4))
plt.plot(m["time"], m[f"{var}_A"], label="point A")
plt.plot(m["time"], m[f"{var}_B"], label="point B", alpha=0.8)
plt.title(f"{var} — time series (A vs B)")
plt.legend()
plt.grid(True)
fname = os.path.join(out_dir, f"time_{var}.png")
plt.savefig(fname, bbox_inches="tight")
plt.close()
plt.figure(figsize=(5,5))
plt.scatter(m[f"{var}_A"], m[f"{var}_B"], alpha=0.7)
mn = np.nanmin([m[f"{var}_A"].min(), m[f"{var}_B"].min()])
mx = np.nanmax([m[f"{var}_A"].max(), m[f"{var}_B"].max()])
plt.plot([mn,mx],[mn,mx], color="k", linestyle="--")
plt.xlabel("A"); plt.ylabel("B"); plt.title(f"{var} scatter A vs B")
plt.grid(True)
plt.savefig(os.path.join(out_dir, f"scatter_{var}.png"),
bbox_inches="tight")
plt.close()
for var in vars_to_check:
plot_time_series_var(monthlyA, monthlyB, var, OUT_DIR)
with open(os.path.join(OUT_DIR, "report_summary.txt"), "w") as f:
f.write("Comparison summary between two farthest points in the largest
community\n\n")
f.write(f"Selected community id: {largest_id}\n")
f.write(f"Point A (lon,lat): {ptA}\n")
f.write(f"Point B (lon,lat): {ptB}\n")
f.write(f"Geodetic distance (m): {maxd}\n\n")
f.write("Metrics:\n")
f.write(metrics.to_string(index=False))
f.write("\n\nNotes:\n- see png files for time series and scatter plots in
this folder.\n")
print("All done. Outputs saved to", OUT_DIR)
93
import os
import math
from PIL import Image
import matplotlib.pyplot as plt
folder =
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\weather_check_results"
# ← тут твоя папка з png
files = [os.path.join(folder, f) for f in os.listdir(folder) if
f.endswith(".png") and f.startswith("time_")]
files = sorted(files)
n = len(files)
cols = 4
rows = math.ceil(n / cols)
fig, axes = plt.subplots(rows, cols, figsize=(20, 5 * rows))
for ax, img_path in zip(axes.flatten(), files):
img = Image.open(img_path)
ax.imshow(img)
ax.set_title(os.path.basename(img_path), fontsize=10)
ax.axis("off")
for ax in axes.flatten()[len(files):]:
ax.axis("off")
plt.tight_layout()
plt.savefig("combined_plots.png", dpi=200)
plt.show()
# create_data_structure.ipynb
MANIFEST_PATH =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv"
import pandas as pd
data = pd.read_csv(MANIFEST_PATH)
data.head()
data[data["valid"] == True]
data_2021 = data[(data["year"] == 2021) & (data["valid"] == True)]
data_2022 = data[(data["year"] == 2022) & (data["valid"] == True)]
data_2023 = data[(data["year"] == 2023) & (data["valid"] == True)]
94
data_2021 =
data_2021[data_2021.groupby("field_id")["month"].transform("count") == 12]
data_2022 =
data_2022[data_2022.groupby("field_id")["month"].transform("count") == 12]
data_2023 =
data_2023[data_2023.groupby("field_id")["month"].transform("count") == 12]
# getting_vari.ipynb
import pandas as pd
print(data.columns)
data.head()
import pandas as pd
import numpy as np
import tifffile
import os
# VARI = (G - R) / (G + R - B)
def compute_vari(img):
if img.ndim == 2:
return np.nan
if img.shape[2] < 3:
return np.nan
R = img[:,:,0].astype(np.float32)
G = img[:,:,1].astype(np.float32)
B = img[:,:,2].astype(np.float32)
denom = (G + R - B)
denom[denom == 0] = np.nan
vari = (G - R) / denom
return np.nanmean(vari)
def add_vari_to_dataframe(df):
vari_values = []
for idx, row in df.iterrows():
path = row["image_path"]
95
if pd.isna(path) or path is None or not isinstance(path, str) or not
os.path.exists(path):
vari_values.append(np.nan)
continue
try:
img = tifffile.imread(path)
if img.ndim == 3 and img.shape[0] in (1,3,4) and img.shape[1] !=
3:
img = np.transpose(img, (1,2,0))
img = np.squeeze(img)
if img.ndim == 2:
img = img[:, :, np.newaxis]
vari = compute_vari(img)
vari_values.append(vari)
except Exception as e:
print(f"Помилка читання {path}: {e}")
vari_values.append(np.nan)
df_copy = df.copy()
df_copy["VARI"] = vari_values
return df_copy
path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv"
data = pd.read_csv(path)
processed_df = add_vari_to_dataframe(data)
save_path =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv"
processed_df.to_csv(save_path, index=False)
print("CSV збережено в:")
print(save_path)
processed_df["VARI"].describe()
# harvest_data.ipynb
96
import pandas as pd
import os
HARVEST_DATA_FOLDER =
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\harvest data"
harvest_data_by_regions = os.path.join(HARVEST_DATA_FOLDER,
"harvest_by_region.csv")
by_regions = pd.read_csv(harvest_data_by_regions, sep=";")
by_regions[by_regions["attributes"] == "Черкаська"]
by_regions.attributes.unique()
# train.ipynb
from tqdm import tqdm
import os
import time
import math
import glob
from typing import List, Tuple, Optional, Callable, Dict
import numpy as np
import pandas as pd
from PIL import Image, UnidentifiedImageError
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, random_split
from torchvision import transforms, models
from sklearn.model_selection import train_test_split
CONFIG = {
"data_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data",
"manifest_path":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv",
"weather_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather",
97
"image_size": (128, 128),
"batch_size": 2,
"num_workers": 0,
"device": "cuda" if torch.cuda.is_available() else "cpu",
"seed": 42,
"pretrain_epochs": 50,
"finetune_epochs": 30,
"lr": 1e-3,
"train_frac": 0.8,
"require_full_12_months": True,
"save_dir": "./checkpoints",
"projection_dim": 256,
"latent_img_dim": 128,
"latent_tab_dim": 128,
"latent_z_dim": 256,
"max_train_samples": 50,
"max_val_samples": 20,
}
def set_seed(seed: int):
import random
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
def makedir(path: str):
os.makedirs(path, exist_ok=True)
def load_manifest(manifest_path: str) -> pd.DataFrame:
df = pd.read_csv(manifest_path)
return df
def filter_full_years(df: pd.DataFrame, year: int) -> pd.DataFrame:
sub = df[(df["year"] == year) & (df["valid"] == True)].copy()
counts = sub.groupby("field_id")["month"].transform("count")
filtered = sub[counts == 12].reset_index(drop=True)
return filtered
def monthly_aggregate_weather(df):
df = df.copy()
98
if "month" not in df.columns:
df["month"] = pd.to_datetime(df["date"]).dt.month
agg = df.groupby("month").agg({
"temperature_2m_mean": "mean",
"precipitation_sum": "sum",
"rain_sum": "sum",
"snowfall_sum": "sum",
"sunshine_duration": "mean",
"relative_humidity_2m_mean": "mean",
"cloud_cover_mean": "mean",
"surface_pressure_mean": "mean",
"wind_speed_10m_mean": "mean",
"soil_temperature_0_to_7cm_mean": "mean",
"soil_moisture_0_to_7cm_mean": "mean"
})
agg = agg.rename(columns=lambda c: f"agg_{c}")
for m in range(1, 13):
if m not in agg.index:
agg.loc[m] = np.zeros(len(agg.columns))
agg = agg.sort_index()
return agg
class FieldMonthlyDataset(Dataset):
def __init__(
self,
manifest_df: pd.DataFrame,
weather_root: str,
years: List[int],
transform_img: Optional[Callable] = None,
require_full_year: bool = True,
months_stack: int = 1,
weather_agg_fn: Optional[Callable] = None,
):
self.manifest = manifest_df.copy().reset_index(drop=True)
self.weather_root = weather_root
self.transform_img = transform_img
self.require_full_year = require_full_year
self.months_stack = months_stack
self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn
self.weather_cache: Dict[Tuple[str, int], np.ndarray] = {}
self._prepare_weather_cache(years)
99
def default_weather_agg_fn(self, monthly_df: pd.DataFrame) -> np.ndarray:
numeric = monthly_df.select_dtypes(include=[np.number])
return numeric.fillna(0).values
def _load_weather_file(self, koatuu: str, year: int) ->
Optional[pd.DataFrame]:
path_pattern = os.path.join(self.weather_root, str(year),
f"{koatuu}*")
files = glob.glob(path_pattern)
if not files:
return None
path = files[0]
try:
df = pd.read_csv(path)
return df
except Exception:
return None
def _prepare_weather_cache(self, years: List[int]):
for _, row in self.manifest.iterrows():
ko = str(row["koatuu"])
yr = int(row["year"])
key = (ko, yr)
if key in self.weather_cache:
continue
if yr not in years:
continue
df = self._load_weather_file(ko, yr)
if df is None:
self.weather_cache[key] = None
continue
monthly = monthly_aggregate_weather(df)
vecs = self.weather_agg_fn(monthly)
self.weather_cache[key] = vecs
def __len__(self):
return len(self.manifest)
def __getitem__(self, idx):
row = self.manifest.iloc[idx]
img_path = row["image_path"]
try:
im = Image.open(img_path).convert("RGB")
except (UnidentifiedImageError, OSError):
import tifffile
im_np = tifffile.imread(img_path)
if im_np.dtype != np.uint8:
100
im_np = (im_np / im_np.max() * 255).astype(np.uint8)
if im_np.ndim == 2:
im_np = np.stack([im_np]*3, axis=-1)
elif im_np.shape[2] > 3:
im_np = im_np[:, :, :3]
im = Image.fromarray(im_np)
if self.transform_img:
im_t = self.transform_img(im)
else:
im_t = transforms.ToTensor()(im)
images_stack = im_t.unsqueeze(0)
ko = str(row["koatuu"])
yr = int(row["year"])
mo = int(row["month"])
cache = self.weather_cache.get((ko, yr), None)
if cache is None:
tab_t = np.zeros((1, 1), dtype=np.float32)
tab_t1 = np.zeros((1, 1), dtype=np.float32)
else:
idx0 = mo - 1
idx1 = (mo % 12)
tab_t = cache[idx0].astype(np.float32)
tab_t1 = cache[idx1].astype(np.float32)
y_next = None
return {
"images_stack": images_stack,
"tab_t": torch.from_numpy(tab_t),
"tab_t1": torch.from_numpy(tab_t1),
"meta": row.to_dict(),
}
class SimpleImageEncoder(nn.Module):
def __init__(self, out_dim=128):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1), nn.ReLU(),
nn.BatchNorm2d(32),
nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(),
nn.BatchNorm2d(64),
nn.AdaptiveAvgPool2d((1, 1)),
)
self.fc = nn.Linear(64, out_dim)
101
def forward(self, x):
h = self.conv(x).view(x.size(0), -1)
return self.fc(h)
class SimpleTabEncoder(nn.Module):
def __init__(self, in_month_dim, out_dim=128):
super().__init__()
self.pool = nn.AdaptiveAvgPool1d(1)
self.fc = nn.Sequential(nn.Linear(in_month_dim, out_dim), nn.ReLU())
def forward(self, x):
if x.dim() == 3:
x = x.mean(dim=1)
return self.fc(x)
class ProjectionHead(nn.Module):
def __init__(self, in_dim, proj_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, in_dim),
nn.ReLU(),
nn.Linear(in_dim, proj_dim)
)
def forward(self, x):
return self.net(x)
class ForecastHead(nn.Module):
def __init__(self, z_dim, tab_dim, hidden=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(z_dim + tab_dim, hidden),
nn.ReLU(),
nn.Linear(hidden, 1)
)
def forward(self, z, tab):
x = torch.cat([z, tab], dim=-1)
return self.net(x).squeeze(-1)
def info_nce_loss(z_a: torch.Tensor, z_b: torch.Tensor, temperature=0.1):
z_a = F.normalize(z_a, dim=1)
z_b = F.normalize(z_b, dim=1)
102
batch_size = z_a.size(0)
representations = torch.cat([z_a, z_b], dim=0) # 2B x D
similarity = torch.matmul(representations, representations.T) # 2B x 2B
labels = torch.arange(batch_size).to(z_a.device)
labels = torch.cat([labels, labels], dim=0)
logits = similarity / temperature
logits_mask = (~torch.eye(2 * batch_size,
dtype=torch.bool)).to(z_a.device)
logits = logits.masked_select(logits_mask).view(2 * batch_size, -1)
positives = torch.cat([torch.sum(z_a * z_b, dim=1), torch.sum(z_b * z_a,
dim=1)], dim=0) / temperature
targets = torch.cat([torch.arange(batch_size) + 0 for _ in range(2)],
dim=0).to(z_a.device)
exp_logits = torch.exp(logits)
denom = exp_logits.sum(dim=1)
sim = torch.matmul(representations, representations.T) / temperature
pos_mask = torch.zeros_like(sim)
for i in range(batch_size):
pos_mask[i, i + batch_size] = 1
pos_mask[i + batch_size, i] = 1
numerator = (torch.exp(sim) * pos_mask).sum(dim=1)
loss = -torch.log(numerator / (torch.exp(sim).sum(dim=1) -
torch.exp(torch.diag(sim))))
loss = loss.mean()
return loss
def pretrain_contrastive(
img_encoder: nn.Module,
tab_encoder: nn.Module,
projection: nn.Module,
dataloader: DataLoader,
epochs: int,
device: str,
lr: float,
save_path: str,
):
makedir(save_path)
img_encoder.to(device)
tab_encoder.to(device)
projection.to(device)
params = list(img_encoder.parameters()) + list(tab_encoder.parameters())
+ list(projection.parameters())
optim = torch.optim.Adam(params, lr=lr)
start_time = time.time()
103
for epoch in range(1, epochs + 1):
epoch_loss = 0.0
n_batches = 0
t0 = time.time()
img_encoder.train(); tab_encoder.train(); projection.train()
for batch in tqdm(dataloader, desc=f"Pretrain epoch
{epoch}/{epochs}", ncols=100):
images_stack = batch["images_stack"]
tab_t = batch["tab_t"]
B = images_stack.size(0)
img = images_stack[:, 0, :, :, :].to(device)
aug = transforms.Compose([
transforms.RandomResizedCrop(CONFIG["image_size"]),
transforms.RandomHorizontalFlip(),
transforms.ToTensor()
])
view1 = img + 0.01 * torch.randn_like(img)
view2 = img + 0.01 * torch.randn_like(img)
tab = tab_t.float().to(device)
tab_view1 = tab + 0.01 * torch.randn_like(tab)
tab_view2 = tab + 0.01 * torch.randn_like(tab)
z_img1 = img_encoder(view1)
z_tab1 = tab_encoder(tab_view1)
z1 = torch.cat([z_img1, z_tab1], dim=1)
p1 = projection(z1)
z_img2 = img_encoder(view2)
z_tab2 = tab_encoder(tab_view2)
z2 = torch.cat([z_img2, z_tab2], dim=1)
p2 = projection(z2)
loss = info_nce_loss(p1, p2)
optim.zero_grad()
loss.backward()
optim.step()
epoch_loss += loss.item()
n_batches += 1
epoch_time = time.time() - t0
print(f"[Pretrain] Epoch {epoch}/{epochs} loss={epoch_loss /
max(1,n_batches):.4f} time={epoch_time:.1f}s")
if epoch % 10 == 0:
torch.save({
"img_encoder": img_encoder.state_dict(),
104
"tab_encoder": tab_encoder.state_dict(),
"projection": projection.state_dict(),
"optimizer": optim.state_dict(),
"epoch": epoch
}, os.path.join(save_path, f"pretrain_epoch_{epoch}.pt"))
total = time.time() - start_time
print("Pretraining finished in {:.1f}s".format(total))
def finetune_forecast(
img_encoder: nn.Module,
tab_encoder: nn.Module,
projection: nn.Module,
forecast_head: nn.Module,
dataloader: DataLoader,
epochs: int,
device: str,
lr: float,
save_path: str,
):
makedir(save_path)
img_encoder.to(device); tab_encoder.to(device); projection.to(device);
forecast_head.to(device)
params = list(forecast_head.parameters())
params += list(projection.parameters())
optimizer = torch.optim.Adam(params, lr=lr)
criterion = nn.MSELoss()
start_time = time.time()
for epoch in range(1, epochs + 1):
epoch_loss = 0.0
n_batches = 0
t0 = time.time()
img_encoder.train(); tab_encoder.train(); projection.train();
forecast_head.train()
for batch in dataloader:
images_stack = batch["images_stack"].to(device)
tab_t = batch["tab_t"].to(device)
tab_t1 = batch["tab_t1"].to(device)
y_next = batch.get("y_next", None)
if y_next is None:
continue
y_next = y_next.float().to(device)
img = images_stack[:, 0, :, :, :]
z_img = img_encoder(img)
z_tab = tab_encoder(tab_t)
105
z = torch.cat([z_img, z_tab], dim=1)
z_proj = projection(z)
tab_feat = tab_t1.view(tab_t1.size(0), -1)
y_hat = forecast_head(z_proj, tab_feat)
loss = criterion(y_hat, y_next)
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
n_batches += 1
epoch_time = time.time() - t0
print(f"[Finetune] Epoch {epoch}/{epochs} loss={epoch_loss /
max(1,n_batches):.6f} time={epoch_time:.1f}s")
if epoch % 5 == 0:
torch.save({
"projection": projection.state_dict(),
"forecast_head": forecast_head.state_dict(),
"optimizer": optimizer.state_dict(),
"epoch": epoch
}, os.path.join(save_path, f"finetune_epoch_{epoch}.pt"))
total = time.time() - start_time
print("Finetune finished in {:.1f}s".format(total))
def build_dataloaders(manifest_path, weather_root, year, cfg):
manifest = load_manifest(manifest_path)
if cfg["require_full_12_months"]:
manifest = filter_full_years(manifest, year)
if cfg["train_frac"] < 1.0:
manifest_train, manifest_val = train_test_split(
manifest,
train_size=cfg["train_frac"],
random_state=cfg["seed"]
)
else:
manifest_train = manifest.copy()
manifest_val = pd.DataFrame([])
if cfg.get("max_train_samples") is not None:
manifest_train = manifest_train.sample(
min(cfg["max_train_samples"], len(manifest_train)),
106
random_state=42
)
if cfg.get("max_val_samples") is not None and not manifest_val.empty:
manifest_val = manifest_val.sample(
min(cfg["max_val_samples"], len(manifest_val)),
random_state=42
)
if len(manifest_train) == 0:
raise RuntimeError("Training set is empty after sampling. Reduce
filtering or increase max_train_samples.")
transform_img = transforms.Compose([
transforms.Resize(cfg["image_size"]),
transforms.ToTensor(),
])
ds_train = FieldMonthlyDataset(
manifest_train, weather_root, years=[year],
transform_img=transform_img
)
dl_train = DataLoader(
ds_train,
batch_size=cfg["batch_size"],
shuffle=True,
num_workers=cfg["num_workers"],
drop_last=True
)
if not manifest_val.empty:
ds_val = FieldMonthlyDataset(
manifest_val, weather_root, years=[year],
transform_img=transform_img
)
dl_val = DataLoader(
ds_val,
batch_size=cfg["batch_size"],
shuffle=False,
num_workers=cfg["num_workers"]
)
else:
dl_val = None
print(len(manifest_train), len(manifest_val))
return dl_train, dl_val
107
def main():
set_seed(CONFIG["seed"])
makedir(CONFIG["save_dir"])
dl_train, dl_val = build_dataloaders(CONFIG["manifest_path"],
CONFIG["weather_root"], 2021, CONFIG)
img_enc = SimpleImageEncoder(out_dim=CONFIG["latent_img_dim"])
sample = next(iter(dl_train))
tab_t_sample = sample["tab_t"]
tab_dim = tab_t_sample.numel() // tab_t_sample.size(0) # if (B, k) -> k
tab_dim = tab_t_sample.shape[-1] if tab_t_sample.dim() == 2 else
tab_t_sample.shape[-1]
tab_enc = SimpleTabEncoder(in_month_dim=tab_t_sample.shape[-1],
out_dim=CONFIG["latent_tab_dim"])
projection = ProjectionHead(CONFIG["latent_img_dim"] +
CONFIG["latent_tab_dim"], CONFIG["projection_dim"])
forecast = ForecastHead(CONFIG["projection_dim"],
tab_t_sample.view(tab_t_sample.size(0), -1).shape[1])
pretrain_contrastive(img_enc, tab_enc, projection, dl_train,
CONFIG["pretrain_epochs"], CONFIG["device"], CONFIG["lr"],
CONFIG["save_dir"])
finetune_forecast(img_enc, tab_enc, projection, forecast, dl_train,
CONFIG["finetune_epochs"], CONFIG["device"], CONFIG["lr"],
CONFIG["save_dir"])
if __name__ == "__main__":
main()
import numpy as np
import tifffile
from PIL import Image
def tiff_to_png(input_path, output_path, rescale_to_8bit=True):
img = tifffile.imread(input_path)
print("orig.shape =", img.shape, "orig.dtype =", img.dtype)
if img.ndim == 3 and img.shape[0] in (1,3,4) and (img.shape[1] != 3):
108
img = np.transpose(img, (1,2,0))
print("transpose (C,H,W)->(H,W,C) ->", img.shape)
img = np.squeeze(img)
print("squeezed.shape =", img.shape)
if img.ndim == 2:
img = img[:, :, np.newaxis]
print("added channel axis ->", img.shape)
H, W, C = img.shape
if C not in (1,3,4):
print("Warning: unexpected channel count:", C)
if img.dtype == np.uint16:
if rescale_to_8bit:
vmax = float(img.max()) if img.max() > 0 else 1.0
img8 = (img.astype(np.float32) / vmax *
255.0).round().clip(0,255).astype(np.uint8)
else:
img8 = (img >> 8).astype(np.uint8)
elif img.dtype == np.float32 or img.dtype == np.float64:
vmin = float(img.min())
vmax = float(img.max())
if vmax <= 1.0:
img8 = (img * 255.0).round().clip(0,255).astype(np.uint8)
else:
denom = (vmax - vmin) if (vmax > vmin) else 1.0
img8 = ((img - vmin) / denom *
255.0).round().clip(0,255).astype(np.uint8)
elif img.dtype == np.uint8:
img8 = img
else:
img8 = (img.astype(np.float32) - float(img.min()))
mn = img8.min()
mx = img8.max() if img8.max() > mn else mn + 1.0
img8 = ((img8 - mn) / (mx - mn) *
255.0).round().clip(0,255).astype(np.uint8)
if img8.shape[2] == 1:
img8 = np.repeat(img8, 3, axis=2)
im = Image.fromarray(img8)
im.save(output_path)
print("Saved:", output_path, "-> shape", img8.shape, "dtype", img8.dtype)
109
input_path =
'C:\\Users\\nazar\\Desktop\\projects\\masters_thesis\\data\\processed_fields\
\processed_data\\images\\2021\\12\\FIELD_44902_12_2021.tif'
output_path = input_path.replace(".tif", ".png")
tiff_to_png(input_path, output_path)
import pandas as pd
manif =
pd.read_csv(r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_f
ields\manifest_fields.csv")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
years = [2021, 2022, 2023]
for ax, year in zip(axes, years):
subset = manif[manif["year"] == year]["valid"]
ax.hist(subset, bins=[-0.5, 0.5, 1.5])
ax.set_title(f"{year}")
ax.set_xticks([0, 1])
ax.set_xticklabels(["False", "True"])
fig.suptitle("Розподіл valid по роках")
plt.tight_layout()
plt.show()
%matplotlib inline
from torchvision.models import efficientnet_b0
import os, time, itertools, glob
from typing import List, Dict, Callable, Optional, Tuple
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, models
from PIL import Image, UnidentifiedImageError
110
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
CONFIG = {
"data_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data",
"manifest_path":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv",
"weather_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather",
"image_size": (128, 128),
"batch_size": 8,
"num_workers": 0,
"device": "cuda" if torch.cuda.is_available() else "cpu",
"seed": 42,
"pretrain_epochs_search": 5,
"pretrain_epochs_full": 50,
"lr": 1e-3,
"train_frac": 0.8,
"require_full_12_months": True,
"save_dir": "./training_result",
"projection_dim": 128,
"latent_img_dim": 64,
"latent_tab_dim": 64,
"max_train_samples": 2000,
"max_val_samples": 200,
"n_visualize_embeddings": 200,
}
os.makedirs(CONFIG["save_dir"], exist_ok=True)
def set_seed(seed:int):
import random
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
set_seed(CONFIG["seed"])
def makedir(path):
os.makedirs(path, exist_ok=True)
def load_manifest(manifest_path: str) -> pd.DataFrame:
111
df = pd.read_csv(manifest_path)
return df
def filter_full_years(df: pd.DataFrame, year: int) -> pd.DataFrame:
sub = df[(df["year"] == year) & (df["valid"] == True)].copy()
counts = sub.groupby("field_id")["month"].transform("count")
filtered = sub[counts == 12].reset_index(drop=True)
return filtered
def monthly_aggregate_weather(df):
df = df.copy()
if "month" not in df.columns:
df["month"] = pd.to_datetime(df["date"]).dt.month
agg = df.groupby("month").agg({
"temperature_2m_mean": "mean",
"precipitation_sum": "sum",
"rain_sum": "sum",
"snowfall_sum": "sum",
"sunshine_duration": "mean",
"relative_humidity_2m_mean": "mean",
"cloud_cover_mean": "mean",
"surface_pressure_mean": "mean",
"wind_speed_10m_mean": "mean",
"soil_temperature_0_to_7cm_mean": "mean",
"soil_moisture_0_to_7cm_mean": "mean"
})
agg = agg.rename(columns=lambda c: f"agg_{c}")
for m in range(1,13):
if m not in agg.index:
agg.loc[m] = np.zeros(len(agg.columns))
agg = agg.sort_index()
return agg
class FieldMonthlyDataset(Dataset):
def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years:
List[int],
transform_img=None, require_full_year=True,
months_stack:int=1, weather_agg_fn=None):
self.manifest = manifest_df.copy().reset_index(drop=True)
self.weather_root = weather_root
self.transform_img = transform_img
self.months_stack = months_stack
self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn
self.weather_cache = {}
self._prepare_weather_cache(years)
def default_weather_agg_fn(self, monthly_df):
112
numeric = monthly_df.select_dtypes(include=[np.number])
return numeric.fillna(0).values
def _load_weather_file(self, koatuu: str, year:int):
path_pattern = os.path.join(self.weather_root, str(year),
f"{koatuu}*")
files = glob.glob(path_pattern)
if not files:
return None
try:
return pd.read_csv(files[0])
except Exception:
return None
def _prepare_weather_cache(self, years: List[int]):
for _, row in self.manifest.iterrows():
ko = str(row["koatuu"])
yr = int(row["year"])
key = (ko, yr)
if key in self.weather_cache: continue
if yr not in years: continue
df = self._load_weather_file(ko, yr)
if df is None:
self.weather_cache[key] = None
else:
monthly = monthly_aggregate_weather(df)
self.weather_cache[key] = self.weather_agg_fn(monthly)
def __len__(self): return len(self.manifest)
def __getitem__(self, idx):
row = self.manifest.iloc[idx]
img_path = row["image_path"]
try:
im = Image.open(img_path).convert("RGB")
except (UnidentifiedImageError, OSError):
import tifffile
im_np = tifffile.imread(img_path)
if im_np.dtype != np.uint8:
im_np = (im_np / max(1, im_np.max()) * 255).astype(np.uint8)
if im_np.ndim == 2:
im_np = np.stack([im_np]*3, axis=-1)
elif im_np.shape[2] > 3:
im_np = im_np[:, :, :3]
im = Image.fromarray(im_np)
if self.transform_img:
113
im_t = self.transform_img(im)
else:
im_t = transforms.ToTensor()(im)
images_stack = im_t.unsqueeze(0) # (1, C, H, W)
ko = str(row["koatuu"]); yr = int(row["year"]); mo =
int(row["month"])
cache = self.weather_cache.get((ko, yr), None)
if cache is None:
tab_t = np.zeros((1,1), dtype=np.float32)
tab_t1 = np.zeros((1,1), dtype=np.float32)
else:
idx0 = mo - 1
idx1 = (mo % 12)
tab_t = cache[idx0].astype(np.float32)
tab_t1 = cache[idx1].astype(np.float32)
out = {
"images_stack": images_stack,
"tab_t": torch.from_numpy(tab_t),
"tab_t1": torch.from_numpy(tab_t1),
"meta": row.to_dict()
}
return out
class EfficientNetEncoder(nn.Module):
def __init__(self, out_dim=64, pretrained=False):
super().__init__()
net = efficientnet_b0(pretrained=pretrained)
self.backbone = net.features
self.pool = nn.AdaptiveAvgPool2d((1,1))
self.fc = nn.Linear(net.classifier[1].in_features, out_dim)
def forward(self, x):
h = self.backbone(x)
h = self.pool(h).view(h.size(0), -1)
return self.fc(h)
class ResNet18Encoder(nn.Module):
def __init__(self, out_dim=64, pretrained=False):
super().__init__()
net = models.resnet18(pretrained=pretrained)
net.fc = nn.Identity()
self.backbone = net
self.fc = nn.Linear(512, out_dim)
def forward(self,x):
114
h = self.backbone(x)
return self.fc(h)
class TabTransformerEncoder(nn.Module):
def __init__(self, in_dim, out_dim=64, num_layers=2, num_heads=4,
hidden_dim=128):
super().__init__()
self.input_proj = nn.Linear(in_dim, hidden_dim)
encoder_layer = nn.TransformerEncoderLayer(
d_model=hidden_dim,
nhead=num_heads,
dim_feedforward=hidden_dim * 2,
batch_first=True,
activation="gelu"
)
self.transformer = nn.TransformerEncoder(encoder_layer,
num_layers=num_layers)
self.fc_out = nn.Linear(hidden_dim, out_dim)
def forward(self, x):
if x.dim() == 2:
x = x.unsqueeze(1)
h = self.input_proj(x)
h = self.transformer(h)
h = h.mean(dim=1)
return self.fc_out(h)
class SmallMLPTab(nn.Module):
def __init__(self, in_dim, out_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, 128), nn.ReLU(),
nn.Linear(128, out_dim)
)
def forward(self,x):
if x.dim()==3:
x = x.mean(dim=1)
return self.net(x)
class CNNMediumEncoder(nn.Module):
def __init__(self, out_dim=64):
super().__init__()
self.conv = nn.Sequential(
115
nn.Conv2d(3,32,3,stride=1,padding=1), nn.ReLU(),
nn.BatchNorm2d(32),
nn.Conv2d(32,64,3,stride=2,padding=1), nn.ReLU(),
nn.BatchNorm2d(64),
nn.Conv2d(64,128,3,stride=2,padding=1), nn.ReLU(),
nn.BatchNorm2d(128),
nn.AdaptiveAvgPool2d((1,1))
)
self.fc = nn.Linear(128, out_dim)
def forward(self,x):
h = self.conv(x).view(x.size(0),-1)
return self.fc(h)
class TabMediumEncoder(nn.Module):
def __init__(self, in_dim, out_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, max(64,in_dim)), nn.ReLU(),
nn.Linear(max(64,in_dim), max(64,in_dim)), nn.ReLU(),
nn.Linear(max(64,in_dim), out_dim)
)
def forward(self,x):
if x.dim()==3:
x = x.mean(dim=1)
return self.net(x)
class ProjectionHead(nn.Module):
def __init__(self, in_dim, proj_dim=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, in_dim), nn.ReLU(),
nn.Linear(in_dim, proj_dim)
)
def forward(self,x): return self.net(x)
class ForecastHead(nn.Module):
def __init__(self, z_dim, tab_dim, hidden=128):
super().__init__()
self.net = nn.Sequential(nn.Linear(z_dim+tab_dim, hidden), nn.ReLU(),
nn.Linear(hidden,1))
def forward(self,z,tab): return self.net(torch.cat([z,tab],dim=-
1)).squeeze(-1)
def info_nce_loss(z_a: torch.Tensor, z_b: torch.Tensor, temperature=0.1):
116
z_a = F.normalize(z_a, dim=1)
z_b = F.normalize(z_b, dim=1)
B = z_a.size(0)
representations = torch.cat([z_a, z_b], dim=0)
sim = torch.matmul(representations, representations.T) / temperature
mask = (~torch.eye(2*B, dtype=torch.bool, device=sim.device))
logits = sim.masked_select(mask).view(2*B, -1)
positives = torch.cat([torch.sum(z_a*z_b, dim=1), torch.sum(z_b*z_a,
dim=1)], dim=0) / temperature
exp_logits = torch.exp(logits)
denom = exp_logits.sum(dim=1)
num = torch.exp(positives)
loss = -torch.log(num / denom)
return loss.mean()
def build_dataloaders(manifest_path, weather_root, year, cfg):
manifest = load_manifest(manifest_path)
if cfg["require_full_12_months"]:
manifest = filter_full_years(manifest, year)
if cfg.get("max_train_samples") is not None:
manifest = manifest.sample(min(cfg["max_train_samples"],
len(manifest)), random_state=cfg["seed"])
transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]),
transforms.ToTensor()])
ds = FieldMonthlyDataset(manifest, weather_root, years=[year],
transform_img=transform_img)
dl = DataLoader(ds, batch_size=cfg["batch_size"], shuffle=True,
num_workers=cfg["num_workers"], drop_last=True)
return dl, ds
def pretrain_one_epoch(img_encoder, tab_encoder, projection, dataloader,
device, optim):
img_encoder.train(); tab_encoder.train(); projection.train()
epoch_loss = 0.0; n = 0
for batch in dataloader:
images = batch["images_stack"][:,0].to(device)
tab = batch["tab_t"].to(device).float()
view1 = images + 0.01*torch.randn_like(images)
view2 = images + 0.01*torch.randn_like(images)
tab1 = tab + 0.01*torch.randn_like(tab)
tab2 = tab + 0.01*torch.randn_like(tab)
z_img1 = img_encoder(view1); z_tab1 = tab_encoder(tab1)
z_img2 = img_encoder(view2); z_tab2 = tab_encoder(tab2)
z1 = torch.cat([z_img1, z_tab1], dim=1)
z2 = torch.cat([z_img2, z_tab2], dim=1)
p1 = projection(z1); p2 = projection(z2)
loss = info_nce_loss(p1, p2)
optim.zero_grad(); loss.backward(); optim.step()
117
epoch_loss += loss.item(); n += 1
return epoch_loss / max(1,n)
def extract_projections(img_encoder, tab_encoder, projection, dataloader,
device, max_samples=200):
img_encoder.eval(); tab_encoder.eval(); projection.eval()
reps = []
metas = []
with torch.no_grad():
cnt = 0
for batch in dataloader:
images = batch["images_stack"][:,0].to(device)
tab = batch["tab_t"].to(device).float()
z_img = img_encoder(images); z_tab = tab_encoder(tab)
z = torch.cat([z_img, z_tab], dim=1)
p = projection(z)
reps.append(p.cpu().numpy())
metas.extend(batch["meta"])
cnt += p.shape[0]
if cnt >= max_samples:
break
reps = np.vstack(reps)[:max_samples]
return reps, metas
def plot_loss_curves(history: Dict[str, List[float]], out_path=None):
plt.figure(figsize=(8,5))
for name, arr in history.items():
plt.plot(arr, label=name)
plt.xlabel("Epoch"); plt.ylabel("InfoNCE loss"); plt.legend();
plt.grid(True)
if out_path:
plt.savefig(out_path, dpi=150, bbox_inches="tight")
plt.show()
def plot_bar_results(df_results, metric="final_loss", title="Comparison",
out_path=None):
df_sorted = df_results.sort_values(metric)
plt.figure(figsize=(8,5))
plt.barh(df_sorted["arch_name"], df_sorted[metric])
plt.xlabel(metric); plt.title(title); plt.grid(axis="x")
if out_path: plt.savefig(out_path, dpi=150, bbox_inches="tight")
plt.show()
def plot_embedding(reps, labels=None, method="tsne", out_path=None):
if method=="tsne":
emb = TSNE(n_components=2, random_state=0,
init="pca").fit_transform(reps)
else:
118
emb = PCA(n_components=2).fit_transform(reps)
plt.figure(figsize=(6,5))
if labels is None:
plt.scatter(emb[:,0], emb[:,1], s=8)
else:
plt.scatter(emb[:,0], emb[:,1], c=labels, s=8, cmap="tab10")
plt.title(f"{method.upper()} embedding"); plt.axis("off")
if out_path: plt.savefig(out_path, dpi=150, bbox_inches="tight")
plt.show()
def get_img_encoder(name, out_dim):
if name=="efficientnet": return EfficientNetEncoder(out_dim=out_dim)
if name=="resnet18": return ResNet18Encoder(out_dim=out_dim,
pretrained=False)
if name=="cnn_medium": return CNNMediumEncoder(out_dim=out_dim)
raise ValueError(name)
def get_tab_encoder(name, in_dim, out_dim):
if name=="tabtransformer": return TabTransformerEncoder(in_dim,
out_dim=out_dim)
if name=="mlp": return SmallMLPTab(in_dim, out_dim=out_dim)
if name=="tab_medium": return TabMediumEncoder(in_dim, out_dim=out_dim)
raise ValueError(name)
def get_projection(name, in_dim, proj_dim):
return ProjectionHead(in_dim, proj_dim=proj_dim)
def run_pretrain_grid_search(dl_train, ds_train, year, cfg, img_choices,
tab_choices, proj_choices, epochs=None):
results = []
history_all = {}
device = cfg["device"]
sample = next(iter(dl_train))
tab_t_sample = sample["tab_t"]
tab_dim = tab_t_sample.numel() // tab_t_sample.size(0) if
tab_t_sample.dim()==2 else tab_t_sample.shape[-1]
for img_name, tab_name, proj_name in itertools.product(img_choices,
tab_choices, proj_choices):
arch_name = f"{img_name}__{tab_name}__{proj_name}"
print("\n========== RUN:", arch_name)
set_seed(cfg["seed"])
img_enc = get_img_encoder(img_name, cfg["latent_img_dim"]).to(device)
tab_enc = get_tab_encoder(tab_name, tab_dim,
cfg["latent_tab_dim"]).to(device)
projection = get_projection(proj_name,
cfg["latent_img_dim"]+cfg["latent_tab_dim"],
119
cfg["projection_dim"]).to(device)
optimizer = torch.optim.Adam(list(img_enc.parameters()) +
list(tab_enc.parameters()) + list(projection.parameters()), lr=cfg["lr"])
n_epochs = epochs or cfg["pretrain_epochs_search"]
losses = []
start = time.time()
for e in range(1, n_epochs+1):
loss = pretrain_one_epoch(img_enc, tab_enc, projection, dl_train,
device, optimizer)
losses.append(loss)
print(f"Epoch {e}/{n_epochs} loss={loss:.4f}")
dur = time.time()-start
print(f"Finished {arch_name} in {dur:.1f}s final_loss={losses[-
1]:.4f}")
ckpt = {
"img_encoder": img_enc.state_dict(),
"tab_encoder": tab_enc.state_dict(),
"projection": projection.state_dict(),
"optimizer": optimizer.state_dict(),
"arch": arch_name,
"final_loss": losses[-1]
}
torch.save(ckpt, os.path.join(cfg["save_dir"],
f"pretrain_{arch_name}.pt"))
reps, metas = extract_projections(img_enc, tab_enc, projection,
dl_train, device, max_samples=cfg["n_visualize_embeddings"])
results.append({
"arch_name": arch_name,
"img": img_name, "tab": tab_name, "projection": proj_name,
"final_loss": float(losses[-1]),
"loss_curve": losses,
"reps": reps
})
history_all[arch_name] = losses
df = pd.DataFrame(results)
return df, history_all
IMG_CHOICES = ["efficientnet", "cnn_medium", "resnet18"]
TAB_CHOICES = ["tabtransformer", "mlp", "tab_medium"]
PROJ_CHOICES = ["default"]
dl_train, ds_train = build_dataloaders(CONFIG["manifest_path"],
CONFIG["weather_root"], 2021, CONFIG)
df_results, history = run_pretrain_grid_search(dl_train, ds_train, 2021,
CONFIG,
IMG_CHOICES, TAB_CHOICES,
PROJ_CHOICES,
120
epochs=CONFIG["pretrain_epochs_search"])
plot_loss_curves({k:v for k,v in history.items()},
out_path=os.path.join(CONFIG["save_dir"], "loss_curves.png"))
plot_bar_results(df_results, metric="final_loss",
out_path=os.path.join(CONFIG["save_dir"], "bar_final_loss.png"))
best_row = df_results.sort_values("final_loss").iloc[0]
print("Best arch:", best_row["arch_name"], "loss=", best_row["final_loss"])
best_reps = best_row["reps"]
try:
plot_embedding(best_reps, method="tsne",
out_path=os.path.join(CONFIG["save_dir"], "best_tsne.png"))
except Exception as e:
print("TSNE failed, falling back to PCA:", e)
plot_embedding(best_reps, method="pca",
out_path=os.path.join(CONFIG["save_dir"], "best_pca.png"))
display(df_results[["arch_name","final_loss"]].sort_values("final_loss"))
df_results[["arch_name","final_loss"]].to_csv(os.path.join(CONFIG["save_dir"]
, "grid_search_results.csv"), index=False)
print("All done. Saved figures and checkpoints to", CONFIG["save_dir"])
%matplotlib inline
from torchvision.models import efficientnet_b0
import os, time, itertools, glob
from typing import List, Dict, Callable, Optional, Tuple
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, models
from PIL import Image, UnidentifiedImageError
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
121
class FieldMonthlyDataset(Dataset):
def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years:
List[int],
transform_img=None, require_full_year=True,
months_stack:int=1, weather_agg_fn=None):
self.manifest = manifest_df.copy().reset_index(drop=True)
self.weather_root = weather_root
self.transform_img = transform_img
self.months_stack = months_stack
self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn
self.weather_cache = {}
self._prepare_weather_cache(years)
def default_weather_agg_fn(self, monthly_df):
numeric = monthly_df.select_dtypes(include=[np.number])
return numeric.fillna(0).values
def _load_weather_file(self, koatuu: str, year:int):
path_pattern = os.path.join(self.weather_root, str(year),
f"{koatuu}*")
files = glob.glob(path_pattern)
if not files:
return None
try:
return pd.read_csv(files[0])
except Exception:
return None
def _prepare_weather_cache(self, years: List[int]):
for _, row in self.manifest.iterrows():
ko = str(row["koatuu"])
yr = int(row["year"])
key = (ko, yr)
if key in self.weather_cache: continue
if yr not in years: continue
df = self._load_weather_file(ko, yr)
if df is None:
self.weather_cache[key] = None
else:
monthly = monthly_aggregate_weather(df)
self.weather_cache[key] = self.weather_agg_fn(monthly)
def __len__(self): return len(self.manifest)
def __getitem__(self, idx):
row = self.manifest.iloc[idx]
try:
122
img_path = row["image_path"]
try:
im = Image.open(img_path).convert("RGB")
except (UnidentifiedImageError, OSError):
import tifffile
im_np = tifffile.imread(img_path)
if im_np.dtype != np.uint8:
im_np = (im_np / max(1, im_np.max()) *
255).astype(np.uint8)
if im_np.ndim == 2:
im_np = np.stack([im_np]*3, axis=-1)
elif im_np.shape[2] > 3:
im_np = im_np[:, :, :3]
im = Image.fromarray(im_np)
if self.transform_img:
im_t = self.transform_img(im)
else:
im_t = transforms.ToTensor()(im)
images_stack = im_t.unsqueeze(0)
ko = str(row["koatuu"]); yr = int(row["year"]); mo =
int(row["month"])
cache = self.weather_cache.get((ko, yr), None)
if cache is None:
raise ValueError("Weather data missing or corrupt for this
sample.")
idx0 = mo - 1
idx1 = (mo % 12)
tab_t = cache[idx0].astype(np.float32)
tab_t1 = cache[idx1].astype(np.float32)
out = {
"images_stack": images_stack,
"tab_t": torch.from_numpy(tab_t),
"tab_t1": torch.from_numpy(tab_t1),
"meta": row.to_dict()
}
return out
except Exception as e:
return None
def collate_fn_skip_none(batch):
123
batch = list(filter(lambda x: x is not None, batch))
if not batch:
return None
from torch.utils.data._utils.collate import default_collate
if len(batch) == 0:
return None
return default_collate(batch)
CONFIG = {
"data_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data",
"manifest_path":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv",
"weather_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather",
"image_size": (128, 128),
"batch_size": 8,
"num_workers": 0,
"device": "cuda" if torch.cuda.is_available() else "cpu",
"seed": 42,
"pretrain_epochs_search": 50,
"pretrain_epochs_full": 35,
"lr": 1e-3,
"train_frac": 0.8,
"require_full_12_months": True,
"save_dir": "./training_result",
"projection_dim": 128,
"latent_img_dim": 64,
"latent_tab_dim": 64,
"max_train_samples": 10000,
"max_val_samples": 1000,
"n_visualize_embeddings": 200,
}
def build_dataloaders(manifest_path, weather_root, year, cfg):
manifest = load_manifest(manifest_path)
if cfg["require_full_12_months"]:
if cfg.get("max_train_samples") is not None:
manifest = manifest.sample(min(cfg["max_train_samples"],
124
len(manifest)), random_state=cfg["seed"])
transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]),
transforms.ToTensor()])
ds = FieldMonthlyDataset(manifest, weather_root, years=[year],
transform_img=transform_img)
dl = DataLoader(
ds,
batch_size=cfg["batch_size"],
shuffle=True,
num_workers=cfg["num_workers"],
drop_last=True,
collate_fn=collate_fn_skip_none
)
return dl, ds
BEST_IMG_NAME = "cnn_medium"
BEST_TAB_NAME = "tabtransformer"
BEST_PROJ_NAME = "default"
BEST_ARCH_NAME = f"{BEST_IMG_NAME}__{BEST_TAB_NAME}__{BEST_PROJ_NAME}"
TARGET_YEAR = 2021
FULL_PRETRAIN_EPOCHS = CONFIG["pretrain_epochs_full"]
def run_full_pretrain(img_name: str, tab_name: str, proj_name: str, year:
int, cfg: dict):
arch_name = f"{img_name}__{tab_name}__{proj_name}"
device = cfg["device"]
print(f"\n=========================================================")
print(f"Starting Full Pretraining for: {arch_name}")
print(f"Total Epochs: {FULL_PRETRAIN_EPOCHS}, Device: {device}")
print(f"=========================================================")
dl_train, ds_train = build_dataloaders(cfg["manifest_path"],
cfg["weather_root"], year, cfg)
sample = next(iter(dl_train))
tab_t_sample = sample["tab_t"]
tab_dim = tab_t_sample.numel() // tab_t_sample.size(0) if
tab_t_sample.dim()==2 else tab_t_sample.shape[-1]
print(f"Inferred Tabular Input Dimension: {tab_dim}")
set_seed(cfg["seed"])
img_enc = get_img_encoder(img_name, cfg["latent_img_dim"]).to(device)
tab_enc = get_tab_encoder(tab_name, tab_dim,
cfg["latent_tab_dim"]).to(device)
total_latent_dim = cfg["latent_img_dim"] + cfg["latent_tab_dim"]
projection = get_projection(proj_name, total_latent_dim,
125
cfg["projection_dim"]).to(device)
optimizer = torch.optim.Adam(
list(img_enc.parameters()) + list(tab_enc.parameters()) +
list(projection.parameters()),
lr=cfg["lr"]
)
losses = []
best_loss = float('inf')
best_ckpt_path = os.path.join(cfg["save_dir"],
f"pretrain_{arch_name}_best.pt")
start = time.time()
for e in range(1, FULL_PRETRAIN_EPOCHS + 1):
loss = pretrain_one_epoch(img_enc, tab_enc, projection, dl_train,
device, optimizer)
losses.append(loss)
if loss < best_loss:
best_loss = loss
ckpt = {
"img_encoder": img_enc.state_dict(),
"tab_encoder": tab_enc.state_dict(),
"projection": projection.state_dict(),
"optimizer": optimizer.state_dict(),
"arch": arch_name,
"final_loss": loss
}
torch.save(ckpt, best_ckpt_path)
print(f"Epoch {e}/{FULL_PRETRAIN_EPOCHS} loss={loss:.4f} (SAVED
BEST CKPT)")
else:
print(f"Epoch {e}/{FULL_PRETRAIN_EPOCHS} loss={loss:.4f}")
dur = time.time()-start
print(f"Finished Full Pretraining in {dur:.1f}s. Best loss achieved:
{best_loss:.4f}")
print(f"Extracting {cfg['n_visualize_embeddings']} embeddings for
visualization...")
best_ckpt = torch.load(best_ckpt_path, map_location=device)
img_enc.load_state_dict(best_ckpt["img_encoder"])
tab_enc.load_state_dict(best_ckpt["tab_encoder"])
projection.load_state_dict(best_ckpt["projection"])
reps, metas = extract_projections(img_enc, tab_enc, projection, dl_train,
device,
126
max_samples=cfg["n_visualize_embeddings"])
plot_loss_curves({"pretrain_loss": losses},
out_path=os.path.join(cfg["save_dir"],
f"loss_curve_{arch_name}.png"))
try:
plot_embedding(reps, method="tsne",
out_path=os.path.join(cfg["save_dir"],
f"tsne_{arch_name}.png"))
except Exception as e:
print(f"TSNE failed ({e}), falling back to PCA.")
plot_embedding(reps, method="pca",
out_path=os.path.join(cfg["save_dir"],
f"pca_{arch_name}.png"))
return img_enc, tab_enc, projection, best_loss, best_ckpt_path
img_enc_trained, tab_enc_trained, proj_trained, final_loss, ckpt_path =
run_full_pretrain(
BEST_IMG_NAME, BEST_TAB_NAME, BEST_PROJ_NAME, TARGET_YEAR, CONFIG
)
print(f"\nPretraining completed for {BEST_ARCH_NAME}.")
print(f"Best model saved to: {ckpt_path}")
import os
import time
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image, UnidentifiedImageError
import glob
from typing import List, Dict, Tuple, Optional
from tqdm import tqdm
CONFIG = {
"data_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
127
sed_data",
"manifest_path":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv",
"weather_root":
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather",
"image_size": (128, 128),
"batch_size": 8,
"num_workers": 0,
"device": "cuda" if torch.cuda.is_available() else "cpu",
"seed": 42,
"pretrain_epochs_full": 3,
"lr": 1e-3,
"train_frac": 0.8,
"require_full_12_months": True,
"save_dir": "./training_result",
"projection_dim": 128,
"latent_img_dim": 64,
"latent_tab_dim": 64,
"max_train_samples": 5000,
"max_val_samples": 500,
"n_visualize_embeddings": 200,
"target_column": "VARI",
"finetune_epochs": 50,
"finetune_lr": 5e-5,
"finetune_batch_size": 32,
"freeze_encoders": True,
"forecast_head_hidden": 128,
}
BEST_IMG_NAME = "cnn_medium"
BEST_TAB_NAME = "tabtransformer"
BEST_ARCH_NAME = f"{BEST_IMG_NAME}__{BEST_TAB_NAME}"
TARGET_YEAR = 2021
class FieldMonthlyDataset(Dataset):
def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years:
List[int],
transform_img=None, require_full_year=True,
months_stack:int=1,
weather_agg_fn=None, target_column: Optional[str] = None):
self.manifest = manifest_df.copy().reset_index(drop=True)
self.weather_root = weather_root
self.transform_img = transform_img
128
self.months_stack = months_stack
self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn
self.weather_cache = {}
self.target_column = target_column # Додано
self._prepare_weather_cache(years)
def default_weather_agg_fn(self, monthly_df):
numeric = monthly_df.select_dtypes(include=[np.number])
return numeric.fillna(0).values
# ----------------------------------------------------
def _load_weather_file(self, koatuu: str, year:int):
path_pattern = os.path.join(self.weather_root, str(year),
f"{koatuu}*")
files = glob.glob(path_pattern)
if not files:
return None
try:
return pd.read_csv(files[0])
except Exception:
return None
def _prepare_weather_cache(self, years: List[int]):
for _, row in self.manifest.iterrows():
ko = str(row["koatuu"])
yr = int(row["year"])
key = (ko, yr)
if key in self.weather_cache: continue
if yr not in years: continue
df = self._load_weather_file(ko, yr)
if df is None:
self.weather_cache[key] = None
else:
monthly = monthly_aggregate_weather(df)
self.weather_cache[key] = self.weather_agg_fn(monthly)
def __len__(self): return len(self.manifest)
def __getitem__(self, idx):
row = self.manifest.iloc[idx]
try:
img_path = row["image_path"]
im_t = transforms.ToTensor()(Image.new('RGB', (128, 128),
color='black'))
images_stack = im_t.unsqueeze(0)
ko = str(row["koatuu"]); yr = int(row["year"]); mo =
129
int(row["month"])
cache = self.weather_cache.get((ko, yr), None)
if cache is None:
raise ValueError("Weather data missing.")
idx0 = mo - 1
idx1 = (mo % 12)
tab_t = cache[idx0].astype(np.float32)
tab_t1 = cache[idx1].astype(np.float32)
out = {
"images_stack": images_stack,
"tab_t": torch.from_numpy(tab_t),
"tab_t1": torch.from_numpy(tab_t1),
"meta": row.to_dict()
}
target_col = self.target_column
if target_col and target_col in row and
pd.notna(row[target_col]):
target = torch.tensor(row[target_col], dtype=torch.float32)
out["target"] = target.unsqueeze(0)
else:
return None
return out
except Exception as e:
return None
def collate_fn_skip_none(batch):
batch = list(filter(lambda x: x is not None, batch))
if len(batch) == 0:
return None
from torch.utils.data._utils.collate import default_collate
return default_collate(batch)
TARGET_COL_FINAL = "target_next_month"
def prepare_supervised_manifest(manifest_df: pd.DataFrame, target_column:
str) -> pd.DataFrame:
if manifest_df.empty:
return manifest_df
df = manifest_df.sort_values(by=['koatuu', 'year', 'month']).copy()
130
df['field_year_id'] = df['koatuu'].astype(str) + '_' +
df['year'].astype(str)
df[TARGET_COL_FINAL] = df.groupby('field_year_id')[target_column].shift(-
1)
df_prepared = df.dropna(subset=[TARGET_COL_FINAL]).reset_index(drop=True)
print(f"Manifest prepared. Original size: {len(manifest_df)}. Supervised
size: {len(df_prepared)}")
return df_prepared
def build_supervised_dataloaders(manifest_path, weather_root, year, cfg):
manifest = load_manifest(manifest_path)
if cfg["require_full_12_months"]:
manifest = filter_full_years(manifest, year)
manifest = prepare_supervised_manifest(manifest, cfg["target_column"])
train_size = int(cfg["train_frac"] * len(manifest))
manifest_train = manifest.sample(train_size, random_state=cfg["seed"])
manifest_val = manifest.drop(manifest_train.index)
if cfg.get("max_train_samples") is not None:
manifest_train = manifest_train.sample(min(cfg["max_train_samples"],
len(manifest_train)), random_state=cfg["seed"])
if cfg.get("max_val_samples") is not None:
manifest_val = manifest_val.sample(min(cfg["max_val_samples"],
len(manifest_val)), random_state=cfg["seed"])
transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]),
transforms.ToTensor()])
ds_train = FieldMonthlyDataset(manifest_train, weather_root,
years=[year],
transform_img=transform_img,
target_column=cfg["target_column"])
ds_val = FieldMonthlyDataset(manifest_val, weather_root, years=[year],
transform_img=transform_img,
target_column=cfg["target_column"])
dl_train = DataLoader(ds_train, batch_size=cfg["finetune_batch_size"],
shuffle=True,
num_workers=cfg["num_workers"], drop_last=True,
collate_fn=collate_fn_skip_none)
dl_val = DataLoader(ds_val, batch_size=cfg["finetune_batch_size"],
shuffle=False,
131
num_workers=cfg["num_workers"], drop_last=False,
collate_fn=collate_fn_skip_none)
return dl_train, dl_val, ds_train, ds_val
class ForecastHead(nn.Module):
def __init__(self, in_dim, out_dim=1, hidden=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, hidden),
nn.ReLU(),
nn.Linear(hidden, out_dim)
)
def forward(self, z):
return self.net(z).squeeze(-1)
class MTCRForecastModel(nn.Module):
def __init__(self, img_encoder, tab_encoder, forecast_head,
freeze_encoders: bool = True):
super().__init__()
self.img_encoder = img_encoder
self.tab_encoder = tab_encoder
self.forecast_head = forecast_head
self.freeze_encoders = freeze_encoders
if self.freeze_encoders:
print("Freezing image and tabular encoders (Linear Probing).")
for param in self.img_encoder.parameters():
param.requires_grad = False
for param in self.tab_encoder.parameters():
param.requires_grad = False
def forward(self, image_t_input, tab_t_input, tab_t1_input):
self.img_encoder.eval() if self.freeze_encoders else
self.img_encoder.train()
self.tab_encoder.eval() if self.freeze_encoders else
self.tab_encoder.train()
z_img_t = self.img_encoder(image_t_input)
z_tab_t = self.tab_encoder(tab_t_input.float())
z_t = torch.cat([z_img_t, z_tab_t], dim=1)
combined_input = torch.cat([z_t, tab_t1_input.float()], dim=1)
132
prediction = self.forecast_head(combined_input)
return prediction
def finetune_one_epoch(model: MTCRForecastModel, dataloader: DataLoader,
device: str, optim: torch.optim.Optimizer, is_train: bool):
if is_train:
model.train()
else:
model.eval()
epoch_loss = 0.0
n = 0
with torch.set_grad_enabled(is_train):
for batch in dataloader:
if batch is None: continue
images_t = batch["images_stack"][:, 0].to(device)
tab_t = batch["tab_t"].to(device).float()
tab_t1 = batch["tab_t1"].to(device).float()
targets = batch["target"].to(device).float().squeeze(1)
if images_t.size(0) == 0: continue
predictions = model(images_t, tab_t, tab_t1)
loss = F.mse_loss(predictions, targets)
if is_train:
optim.zero_grad()
loss.backward()
optim.step()
epoch_loss += loss.item() * targets.size(0)
n += targets.size(0)
avg_loss = epoch_loss / max(1, n)
return avg_loss
def train_finetune_model(model: MTCRForecastModel, dl_train: DataLoader,
dl_val: DataLoader, cfg: dict, arch_name: str):
device = cfg["device"]
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad,
model.parameters()), lr=cfg["finetune_lr"])
history = {"train_loss": [], "val_loss": []}
best_val_loss = float('inf')
best_epoch = 0
133
ckpt_path_template = os.path.join(cfg["save_dir"],
f"finetune_VARI_{arch_name}_best.pt")
print(f"\n--- Starting Finetuning for {arch_name}
({cfg['finetune_epochs']} epochs) ---")
for e in tqdm(range(1, cfg["finetune_epochs"] + 1), desc=f"Finetune
{arch_name}"):
train_loss = finetune_one_epoch(model, dl_train, device, optimizer,
is_train=True)
val_loss = finetune_one_epoch(model, dl_val, device, None,
is_train=False)
history["train_loss"].append(train_loss)
history["val_loss"].append(val_loss)
print(f"Epoch {e}: Train MSE={train_loss:.4f}, Val
MSE={val_loss:.4f}")
if val_loss < best_val_loss:
best_val_loss = val_loss
best_epoch = e
torch.save({
"model_state_dict": model.state_dict(),
"val_loss": best_val_loss,
"epoch": best_epoch,
"arch": arch_name
}, ckpt_path_template)
print(f"SAVED BEST CKPT at Epoch {e}")
print(f"Finetuning finished. Best Val MSE: {best_val_loss:.4f} at epoch
{best_epoch}")
return history, best_val_loss
if 'img_enc_trained' not in locals() or 'tab_enc_trained' not in locals():
print("WARNING: Encoders not found. Attempting to load best pretrain
checkpoint...")
dl_train_ft, dl_val_ft, ds_train_ft, ds_val_ft =
build_supervised_dataloaders(
CONFIG["manifest_path"], CONFIG["weather_root"], TARGET_YEAR, CONFIG
)
latent_mtcr_dim = CONFIG["latent_img_dim"] + CONFIG["latent_tab_dim"]
try:
134
sample_tab_t1 = next(iter(dl_train_ft))["tab_t1"]
raw_tab_dim = sample_tab_t1.numel() // sample_tab_t1.size(0)
except Exception as e:
print(f"Error: Could not determine raw_tab_dim. Dataloader may be
empty/faulty. Error: {e}")
raise
final_forecast_in_dim = latent_mtcr_dim + raw_tab_dim
print(f"\nForecast Head Input Dimension: {latent_mtcr_dim} (Z_t) +
{raw_tab_dim} (X_tab, t+1) = **{final_forecast_in_dim}**")
forecast_head = ForecastHead(
in_dim=final_forecast_in_dim,
out_dim=1,
hidden=CONFIG["forecast_head_hidden"]
).to(CONFIG["device"])
forecast_model = MTCRForecastModel(
img_enc_trained,
tab_enc_trained,
forecast_head,
freeze_encoders=CONFIG["freeze_encoders"]
).to(CONFIG["device"])
finetune_history, final_val_loss = train_finetune_model(
forecast_model, dl_train_ft, dl_val_ft, CONFIG, BEST_ARCH_NAME
)
print(f"\nФінальне Supervised VARI Forecast Validation MSE для
{BEST_ARCH_NAME}: {final_val_loss:.4f}")
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
def evaluate_and_report_metrics(model: MTCRForecastModel, dl_val: DataLoader,
device: str) -> Dict[str, float]:
model.eval()
all_preds = []
all_targets = []
with torch.no_grad():
for batch in tqdm(dl_val, desc="Evaluating"):
if batch is None: continue
135
images_t = batch["images_stack"][:, 0].to(device)
tab_t = batch["tab_t"].to(device).float()
tab_t1 = batch["tab_t1"].to(device).float()
targets = batch["target"].to(device).float().squeeze(1)
if targets.size(0) == 0: continue
predictions = model(images_t, tab_t, tab_t1)
all_preds.append(predictions.cpu().numpy())
all_targets.append(targets.cpu().numpy())
y_true = np.concatenate(all_targets)
y_pred = np.concatenate(all_preds)
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_true, y_pred)
r2 = r2_score(y_true, y_pred)
return {
"N_samples": len(y_true),
"MSE": mse,
"RMSE": rmse,
"MAE": mae,
"R2": r2,
"y_true": y_true,
"y_pred": y_pred
}
import matplotlib.pyplot as plt
import seaborn as sns
def plot_analysis_charts(y_true: np.ndarray, y_pred: np.ndarray,
title_prefix: str, save_dir: str):
plt.figure(figsize=(8, 6))
sns.scatterplot(x=y_true, y=y_pred, alpha=0.6)
min_val = min(y_true.min(), y_pred.min())
max_val = max(y_true.max(), y_pred.max())
plt.plot([min_val, max_val], [min_val, max_val],
color='red', linestyle='--', label='Ідеальний прогноз (y=x)')
plt.title(f"{title_prefix} Predicted vs. True VARI")
plt.xlabel("Справжнє значення VARI ($y$)")
plt.ylabel("Прогнозоване значення VARI ($\hat{y}$)")
136
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig(os.path.join(save_dir,
f"{title_prefix}_Predicted_vs_True.png"))
plt.close()
residuals = y_true - y_pred
plt.figure(figsize=(8, 6))
sns.scatterplot(x=y_pred, y=residuals, alpha=0.6)
plt.axhline(y=0, color='red', linestyle='-')
plt.title(f"{title_prefix} Аналіз Залишків")
plt.xlabel("Прогнозоване значення VARI ($\hat{y}$)")
plt.ylabel("Залишки ($y - \hat{y}$)")
plt.grid(True, alpha=0.3)
plt.savefig(os.path.join(save_dir, f"{title_prefix}_Residuals_Plot.png"))
plt.close()
ckpt_path = os.path.join(CONFIG["save_dir"],
f"finetune_VARI_{BEST_ARCH_NAME}_best.pt")
if not os.path.exists(ckpt_path):
print(f"Error: Checkpoint file not found at {ckpt_path}. Ensure
finetuning ran correctly.")
else:
print(f"\n--- Завантаження та Оцінка моделі: {BEST_ARCH_NAME} ---")
best_ckpt = torch.load(ckpt_path, map_location=CONFIG["device"])
forecast_model.load_state_dict(best_ckpt["model_state_dict"])
results = evaluate_and_report_metrics(forecast_model, dl_val_ft,
CONFIG["device"])
print("\n" + "="*50)
print(f"КІНЦЕВА ОЦІНКА НА ВАЛІДАЦІЙНОМУ НАБОРІ ({BEST_ARCH_NAME})")
print(f"Кількість зразків: {results['N_samples']}")
print("-" * 50)
print(f"MSE (Mean Squared Error): {results['MSE']:.6f}")
print(f"RMSE (Root Mean Squared Error): {results['RMSE']:.4f}")
print(f"MAE (Mean Absolute Error): {results['MAE']:.4f}")
print(f"R2 Score (Коефіцієнт детермінації): {results['R2']:.4f}")
print("="*50)
plot_analysis_charts(
137
results['y_true'],
results['y_pred'],
title_prefix=f"VARI_Forecast_{BEST_ARCH_NAME}",
save_dir=CONFIG["save_dir"]
)
print(f"Графіки аналізу збережено у: {CONFIG['save_dir']}")
import torch
import numpy as np
import pandas as pd
def load_best_forecast_model(cfg, arch_name, img_enc, tab_enc,
final_forecast_in_dim):
ckpt_path = os.path.join(cfg["save_dir"],
f"finetune_VARI_{arch_name}_best.pt")
forecast_head = ForecastHead(
in_dim=final_forecast_in_dim,
out_dim=1,
hidden=cfg["forecast_head_hidden"]
).to(cfg["device"])
model = MTCRForecastModel(
img_enc,
tab_enc,
forecast_head,
freeze_encoders=False
).to(cfg["device"])
if os.path.exists(ckpt_path):
best_ckpt = torch.load(ckpt_path, map_location=cfg["device"])
model.load_state_dict(best_ckpt["model_state_dict"])
model.eval()
print(f"Модель прогнозування успішно завантажена з {ckpt_path}.")
else:
print(f"Помилка: Контрольна точка не знайдена за шляхом:
{ckpt_path}")
raise FileNotFoundError(f"Missing checkpoint at {ckpt_path}")
return model
def get_sample_batch(dataloader, n_samples=3):
samples = []
for batch in dataloader:
if batch is None: continue
138
for i in range(batch["images_stack"].size(0)):
sample = {key: batch[key][i].unsqueeze(0) for key in batch if key
not in ["meta"]}
sample["meta"] = {key: batch["meta"][key][i] for key in
batch["meta"]}
samples.append(sample)
if len(samples) >= n_samples:
return samples
return samples
from IPython.display import display
def run_simple_scenario_analysis(model, samples, device):
results = []
T_index, P_index = 0, 1
scenarios = {
"Base_Forecast": (0, 0), # Базовий прогноз (без змін)
"S1_HotDry": (+0.15, -0.15), # +15% T, -15% P
"S2_ColdWet": (-0.15, +0.15), # -15% T, +15% P
"S3_VeryHot": (+0.30, 0), # +30% T, 0% P
}
model.eval()
for sample in samples:
base_tab_t1 = sample["tab_t1"].float().to(device)
base_pred = model(sample["images_stack"][:, 0].to(device),
sample["tab_t"].to(device), base_tab_t1).item()
sample_results = {
"koatuu": sample["meta"]["koatuu"],
"month": sample["meta"]["month"],
"True_VARI": sample["target"].item(),
"Base_Pred_VARI": base_pred
}
for name, (t_shift, p_shift) in scenarios.items():
scenario_tab_t1 = base_tab_t1.clone()
scenario_tab_t1[0, T_index] *= (1 + t_shift)
scenario_tab_t1[0, P_index] *= (1 + p_shift)
scenario_pred = model(sample["images_stack"][:, 0].to(device),
sample["tab_t"].to(device),
139
scenario_tab_t1).item()
sample_results[name] = scenario_pred
results.append(sample_results)
return pd.DataFrame(results)
import pandas as pd
import torch
N_SAMPLES_FOR_SCENARIO = 5
print(f"Завантаження {N_SAMPLES_FOR_SCENARIO} зразків з валідаційного
набору...")
try:
sample_data = get_sample_batch(dl_val_ft,
n_samples=N_SAMPLES_FOR_SCENARIO)
if not sample_data:
print("Не вдалося завантажити жодного зразка. Перевірте, чи не
порожній dl_val_ft.")
else:
print(f"Успішно завантажено {len(sample_data)} зразків.")
scenario_df = run_simple_scenario_analysis(
forecast_model,
sample_data,
CONFIG["device"]
)
print("\n--- Результати Сценарного Аналізу (Перші 5 рядків) ---")
display(scenario_df.head())
except Exception as e:
print(f"Виникла помилка під час отримання зразків або аналізу: {e}")
def run_feature_sensitivity_analysis(model, sample, feature_indices:
List[int], perturbation: float = 0.1):
model.eval()
device = "cuda"
140
base_tab_t1 = sample["tab_t1"].float().to(device)
base_pred = model(sample["images_stack"][:, 0].to(device),
sample["tab_t"].to(device), base_tab_t1).item()
sensitivity_results = {}
for idx in feature_indices:
feature_name = f"Feature_{idx}"
# Сценарій + Perturbation
tab_t1_plus = base_tab_t1.clone()
tab_t1_plus[0, idx] *= (1 + perturbation)
pred_plus = model(sample["images_stack"][:, 0].to(device),
sample["tab_t"].to(device), tab_t1_plus).item()
# Сценарій - Perturbation
tab_t1_minus = base_tab_t1.clone()
tab_t1_minus[0, idx] *= (1 - perturbation)
pred_minus = model(sample["images_stack"][:, 0].to(device),
sample["tab_t"].to(device), tab_t1_minus).item()
impact_plus = (pred_plus - base_pred)
impact_minus = (pred_minus - base_pred)
sensitivity_results[feature_name] = {
"Base_VARI_Pred": base_pred,
"Pred_+": pred_plus,
"Pred_-": pred_minus,
"Impact_+": impact_plus,
"Impact_-": impact_minus,
"Total_Range": pred_plus - pred_minus
}
return sensitivity_results
feature_indices_to_test = [0, 1, 2, 3]
print(f"Аналізуються індекси ознак: {feature_indices_to_test}")
all_sensitivity_results = []
N_SAMPLES_FOR_SCENARIO = 5
sample_data = get_sample_batch(dl_val_ft, n_samples=N_SAMPLES_FOR_SCENARIO)
for i, sample in enumerate(sample_data):
sensitivity_results = run_feature_sensitivity_analysis(
141
model=forecast_model,
sample=sample,
feature_indices=feature_indices_to_test,
perturbation=0.1 # Зміна на +/- 10%
)
sample_id =
f"Sample_{i}_koatuu_{sample['meta']['koatuu']}_month_{sample['meta']['month']
}"
df_temp = pd.DataFrame.from_dict(sensitivity_results, orient='index')
df_temp['ID'] = sample_id
all_sensitivity_results.append(df_temp)
print(f"\n--- Результати Чутливості для Зразка {i} ---")
print(df_temp[['Total_Range', 'Impact_+', 'Impact_-
']].sort_values(by='Total_Range', ascending=False))
final_sensitivity_df = pd.concat(all_sensitivity_results)
print("\nФінальні результати аналізу чутливості готові.")
# download_sentinel_images.js
var region = ee.FeatureCollection("projects/sentinel-images-
477301/assets/cherkassy_boundaries");
var geom = region.geometry().bounds();
Map.centerObject(region);
Map.addLayer(region, {}, 'Cherkasy boundary');
var maskS2Clouds = function(image) {
var scl = image.select('SCL');
var mask = scl.neq(1).and(scl.neq(3)).and(scl.neq(8))
.and(scl.neq(9)).and(scl.neq(10));
return image.updateMask(mask);
};
var year = 2021;
var start = ee.Date(year + '-01-01');
var end = ee.Date(year + '-12-31');
var s2 = ee.ImageCollection("COPERNICUS/S2_SR")
.filterBounds(geom)
.filterDate(start, end)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.map(maskS2Clouds);
142
var months = ee.List.sequence(1, 12);
months.getInfo().forEach(function(m) {
var monthStart = ee.Date.fromYMD(year, m, 1);
var monthEnd = monthStart.advance(1, 'month');
var monthlyMedian = s2.filterDate(monthStart, monthEnd)
.median()
.select(['B4','B3','B2']) // RGB
.toUint16()
.clip(geom);
var monthStr = ('0' + m).slice(-2); // 01, 02, ... 12
var desc = 'Cherkassy_' + year + '_' + monthStr + '_RGB_Median';
Export.image.toDrive({
image: monthlyMedian,
description: desc,
fileNamePrefix: desc,
folder: 'Cherkassy_S2',
scale: 10,
crs: 'EPSG:32636',
region: geom,
maxPixels: 1e13,
fileFormat: 'GeoTIFF',
formatOptions: {cloudOptimized: true}
});
});
Map.addLayer(s2.filterDate('2021-01-01','2021-01-31').median(),
{bands:['B4','B3','B2'], min:0, max:3000}, 'Jan RGB');
# problem_month.js
var region = ee.FeatureCollection("projects/sentinel-images-
477301/assets/cherkassy_boundaries");
var geom = region.geometry().bounds();
Map.centerObject(region);
Map.addLayer(region, {}, 'Cherkasy boundary');
var maskS2Clouds = function(image) {
var scl = image.select('SCL');
var mask = scl.neq(1).and(scl.neq(3)).and(scl.neq(8))
143
.and(scl.neq(9)).and(scl.neq(10));
return image.updateMask(mask);
};
var year = 2021;
var month = 10;
var monthStart = ee.Date.fromYMD(year, month, 1);
var monthEnd = monthStart.advance(1, 'month');
var s2_oct = ee.ImageCollection("COPERNICUS/S2_SR")
.filterBounds(geom)
.filterDate(monthStart, monthEnd)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.map(function(img) {
var bandNames = img.bandNames();
return ee.Algorithms.If(
bandNames.contains('QA10'),
img,
null
);
}, true)
.map(maskS2Clouds);
var monthlyMedian = s2_oct.median()
.select(['B4','B3','B2'])
.toUint16()
.clip(geom);
Map.addLayer(monthlyMedian, {min:0, max:3000}, 'October RGB Median');
Export.image.toDrive({
image: monthlyMedian,
description: 'Cherkassy_2021_10_RGB_Median',
fileNamePrefix: 'Cherkassy_2021_10_RGB_Median',
folder: 'Cherkassy_S2',
scale: 10,
crs: 'EPSG:32636',
region: geom,
maxPixels: 1e13,
fileFormat: 'GeoTIFF',
formatOptions: {cloudOptimized: true}
});