שיטות מומלצות לתכנות

במאמר הזה מתוארות שיטות תכנות שנועדו למקסם את הסיכוי להצלחה של חישובים מורכבים או יקרים ב-Earth Engine. השיטות המתוארות כאן רלוונטיות גם לחישוב אינטראקטיבי (למשל, Code Editor) וגם לחישוב באצווה (Export), אבל בדרך כלל כדאי להריץ חישובים ארוכים במערכת האצווה.

להימנע מערבוב בין פונקציות ואובייקטים של לקוח לבין פונקציות ואובייקטים של שרת

אובייקטים של שרת ב-Earth Engine הם אובייקטים עם קונסטרוקטורים שמתחילים ב-ee (למשל ee.Image,‏ ee.Reducer), וכל השיטות באובייקטים כאלה הן פונקציות של שרת. כל אובייקט שלא נוצר באופן הזה הוא אובייקט לקוח. אובייקטים של לקוח יכולים להגיע מעורך הקוד (למשל Map, ‏ Chart) או משפת JavaScript (למשל Date, ‏ Math, ‏ [], ‏ {}).

כדי להימנע מהתנהגות לא מכוונת, אל תערבבו פונקציות של לקוח ושל שרת בסקריפט, כפי שמתואר כאן, כאן וכאן. בדף הזה ו/או במדריך הזה מוסבר בהרחבה על ההבדל בין לקוח לשרת ב-Earth Engine. הדוגמה הבאה ממחישה את הסכנות שבשילוב בין פונקציונליות של לקוח לבין פונקציונליות של שרת:

שגיאה – הקוד הזה לא עובד!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Won't work.
for(var i=0; i<table.size(); i++) {
  print('No!');
}

רואים את השגיאה? שימו לב ש-table.size() הוא שיטת שרת באובייקט שרת, ואי אפשר להשתמש בו עם פונקציונליות בצד הלקוח, כמו התנאי <.

מצב שבו כדאי להשתמש בלולאות for הוא במהלך הגדרת ממשק המשתמש, כי האובייקטים וה-methods של ui ב-Code Editor נמצאים בצד הלקוח. (מידע נוסף על יצירת ממשקי משתמש ב-Earth Engine) לדוגמה:

שימוש בפונקציות לקוח להגדרת ממשק המשתמש

var panel = ui.Panel();
for(var i=1; i<8; i++) {
  panel.widgets().set(i, ui.Button('button ' + i))
}
print(panel);

לעומת זאת, map() היא פונקציית שרת, ופונקציונליות של לקוח לא תפעל בתוך הפונקציה שמועברת אל map(). לדוגמה:

שגיאה – הקוד הזה לא עובד!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Error:
var foobar = table.map(function(f) {
  print(f); // Can't use a client function here.
  // Can't Export, either.
});

כדי לבצע פעולה כלשהי בכל רכיב באוסף, map() היא פונקציה על האוסף ו-set() הוא מאפיין:

משתמשים ב-map() וב-set()!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
print(table.first());

// Do something to every element of a collection.
var withMoreProperties = table.map(function(f) {
  // Set a property.
  return f.set('area_sq_meters', f.area())
});
print(withMoreProperties.first());

אפשר גם filter() את האוסף על סמך נכסים מחושבים או קיימים, וprint() את התוצאה. לתשומת ליבכם, אי אפשר להדפיס אוסף שמכיל יותר מ-5,000 פריטים. אם מופיעה השגיאה 'Collection query aborted after accumulating over 5000 elements', צריך filter() או limit() את האוסף לפני ההדפסה.

הימנעות ממרה לרשימה ללא צורך

האוספים ב-Earth Engine עוברים עיבוד באמצעות אופטימיזציות שמתבטלות אם ממירים את האוסף לסוג List או Array. אלא אם צריך גישה אקראית לרכיבי האוסף (כלומר צריך לקבל את הרכיב ה-i באוסף), צריך להשתמש במסננים באוסף כדי לגשת לרכיבים ספציפיים באוסף. הדוגמה הבאה ממחישה את ההבדל בין המרה של סוג (לא מומלץ) לבין סינון (מומלץ) כדי לגשת לאלמנט באוסף:

אל תבצעו המרה לרשימה ללא צורך!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!!
var list = table.toList(table.size());
print(list.get(13)); // User memory limit exceeded.

חשוב לזכור שאפשר לגרום בקלות לשגיאות על ידי המרת אוסף לרשימה ללא צורך. הדרך הבטוחה יותר היא להשתמש ב-filter():

שימוש ב-filter()

print(table.filter(ee.Filter.eq('country_na', 'Niger')).first());

חשוב לזכור שצריך להשתמש במסננים מוקדם ככל האפשר בניתוח.

יש להימנע מ-ee.Algorithms.If()

אל תשתמשו ב-ee.Algorithms.If() כדי להטמיע לוגיקה של הסתעפות, במיוחד בפונקציה ממופה. כפי שמוצג בדוגמה הבאה, הפונקציה ee.Algorithms.If() יכולה להיות תובענית מאוד לזיכרון ולא מומלצת:

לא משתמשים ב-If()!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!
var veryBad = table.map(function(f) {
  return ee.Algorithms.If({
    condition: ee.String(f.get('country_na')).compareTo('Chad').gt(0),
    trueCase: f,      // Do something.
    falseCase: null   // Do something else.
  });
}, true);
print(veryBad); // User memory limit exceeded.

// If() may evaluate both the true and false cases.

שימו לב שהארגומנט השני של map() הוא true. המשמעות היא שהפונקציה הממופה עשויה להחזיר ערכים null, והם יוסרו מהאוסף שנוצר. זה יכול להיות שימושי (ללא If()), אבל הפתרון הפשוט ביותר הוא להשתמש במסנן:

שימוש ב-filter()

print(table.filter(ee.Filter.eq('country_na', 'Chad')));

כפי שמתואר במדריך הזה, גישה לתכנות פונקציונלית באמצעות מסננים היא הדרך הנכונה להחיל לוגיקה אחת על חלק מהרכיבים של האוסף ולוגיקה אחרת על שאר הרכיבים של האוסף.

יש להימנע מ-reproject()

אל תשתמשו בהמרת מחדש אלא אם הדבר הכרחי. אחת הסיבות לשימוש ב-reproject() היא לאלץ את החישובים ב-Code Editor להתבצע בקנה מידה ספציפי, כדי שתוכלו לבחון את התוצאות בקנה המידה הרצוי לניתוח. בדוגמה הבאה, מחושב מספר הפיקסלים החמים ומחושב גם מספר הפיקסלים בכל קבוצה. מריצים את הדוגמה ולוחצים על אחת מהתיקונים. שימו לב שמספר הפיקסלים שונה בין הנתונים שהופרויקט מחדש לבין הנתונים שלא הופרויקט מחדש.

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.405, 37.786]);
Map.centerObject(sf, 13);

// A reason to reproject - counting pixels and exploring interactively.
var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.00341802).add(149);  // Apply scale factors.
Map.addLayer(image, {bands: ['ST_B10'], min: 280, max: 317}, 'image');

var hotspots = image.select('ST_B10').gt(317)
  .selfMask()
  .rename('hotspots');
var objectSize = hotspots.connectedPixelCount(256);

Map.addLayer(objectSize, {min: 1, max: 256}, 'Size No Reproject', false);

// Beware of reproject!  Don't zoom out on reprojected data.
var reprojected = objectSize.reproject(hotspots.projection());
Map.addLayer(reprojected, {min: 1, max: 256}, 'Size Reproject', false);

הסיבה לכך היא שהיקף הניתוח מוגדר לפי מרחק התצוגה ב-Code Editor. כשקוראים ל-reproject(), מגדירים את קנה המידה של החישוב במקום Code Editor. מומלץ להשתמש ב-reproject() בזהירות רבה מהסיבות שמפורטות במסמך הזה.

סינון וselect() קודם

באופן כללי, כדאי לסנן אוספים של קלט לפי זמן, מיקום ו/או מטא-נתונים לפני שמבצעים פעולה כלשהי אחרת עם האוסף. כדאי להחיל מסננים סלקטיביים יותר לפני מסננים פחות סלקטיביים. מסננים מרחביים ו/או זמניים הם לרוב סלקטיביים יותר. לדוגמה, שימו לב ש-select() ו-filter() חלים לפני map():

var images = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');
var sf = ee.Geometry.Point([-122.463, 37.768]);

// Expensive function to reduce the neighborhood of an image.
var reduceFunction = function(image) {
  return image.reduceNeighborhood({
    reducer: ee.Reducer.mean(),
    kernel: ee.Kernel.square(4)
  });
};

var bands = ['B4', 'B3', 'B2'];
// Select and filter first!
var reasonableComputation = images
    .select(bands)
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-02-01')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 1))
    .aside(print) // Useful for debugging.
    .map(reduceFunction)
    .reduce('mean')
    .rename(bands);
var viz = {bands: bands, min: 0, max: 10000};
Map.addLayer(reasonableComputation, viz, 'reasonableComputation');

צריך להשתמש ב-updateMask() במקום ב-mask()

ההבדל בין updateMask() לבין mask() הוא שבקוד הראשון מתבצע and() לוגי של הארגומנט (המסכה החדשה) ומסכת התמונה הקיימת, ואילו בקוד השני מתבצעת החלפה פשוטה של מסכת התמונה בארגומנט.mask() הסכנה של האפשרות השנייה היא שאפשר לחשוף בטעות את הפיקסלים. בדוגמה הזו, היעד הוא להסתיר פיקסלים בגובה של עד 300 מטרים. כפי שאפשר לראות (מרחיבים את התצוגה), השימוש ב-mask() גורם להסרת המסכה של הרבה פיקסלים שלא שייכים לתמונה הרלוונטית:

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.40554461769182, 37.786807309873716]);
var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

Map.centerObject(sf, 7);

var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.0000275).subtract(0.2);  // Apply scale factors.
var vis = {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3};
Map.addLayer(image, vis, 'image', false);

var mask = aw3d30.select('AVE').gt(300);
Map.addLayer(mask, {}, 'mask', false);

// NO!  Don't do this!
var badMask = image.mask(mask);
Map.addLayer(badMask, vis, 'badMask');

var goodMask = image.updateMask(mask);
Map.addLayer(goodMask, vis, 'goodMask', false);

שילוב של reducers

אם אתם צריכים כמה נתונים סטטיסטיים (למשל, ממוצע וסטיית תקן) ממקור קלט אחד (למשל, אזור בתמונה), יעיל יותר לשלב בין מפחיתים. לדוגמה, כדי לקבל נתונים סטטיסטיים על תמונות, משלבים בין reducers באופן הבא:

var image = ee.Image(
  'COPERNICUS/S2_HARMONIZED/20150821T111616_20160314T094808_T30UWU');

// Get mean and SD in every band by combining reducers.
var stats = image.reduceRegion({
  reducer: ee.Reducer.mean().combine({
    reducer2: ee.Reducer.stdDev(),
    sharedInputs: true
  }),
  geometry: ee.Geometry.Rectangle([-2.15, 48.55, -1.83, 48.72]),
  scale: 10,
  bestEffort: true // Use maxPixels if you care about scale.
});

print(stats);

// Extract means and SDs to images.
var meansImage = stats.toImage().select('.*_mean');
var sdsImage = stats.toImage().select('.*_stdDev');

בדוגמה הזו, שימו לב שה-reducer של הממוצע משולב עם ה-reducer של סטיית התקן ושהערך של sharedInputs הוא true כדי לאפשר מעבר יחיד דרך הפיקסלים של הקלט. במילון הפלט, שם המצמצם מצורף לשם הלהקה. כדי לקבל תמונות של הממוצע וסטיית התקן (לדוגמה, כדי לנרמל את תמונה הקלט), אפשר להפוך את הערכים לתמונה ולהשתמש בביטויים רגולריים כדי לחלץ את הממוצע וסטיית התקן בנפרד, כפי שמתואר בדוגמה.

שימוש ב-Export

אם נתקלתם בטעויות מסוג 'מגבלת הזיכרון של המשתמש חרגה' או 'התוקף של הזמן הקצוב לחישוב פג' בכלי לעריכת קוד, יכול להיות שתוכלו לבצע את אותן פעולות באמצעות Export. הסיבה לכך היא שהזמן הקצוב לתפוגה ארוך יותר והמידה המותרת של שימוש בזיכרון גדולה יותר כשמריצים את הפקודות במערכת האצווה (שם פועלות הפקודות לייצוא). (יש גישות אחרות שאפשר לנסות קודם, כפי שמפורט במסמך בנושא ניפוי באגים). בהמשך לדוגמה הקודמת, נניח שהמילון החזיר שגיאה. כדי לקבל את התוצאות, אפשר לבצע פעולה כמו:

var link = '86836482971a35a5e735a17e93c23272';
Export.table.toDrive({
  collection: ee.FeatureCollection([ee.Feature(null, stats)]),
  description: 'exported_stats_demo_' + link,
  fileFormat: 'CSV'
});

חשוב לזכור שהקישור מוטמע בשם הנכס, כדי שניתן יהיה לשחזר אותו. חשוב גם לזכור שאם רוצים לייצא את toAsset, צריך לספק גיאומטריה. היא יכולה להיות כל דבר, למשל מרכז הכובד של התמונה, שקל וזול לחשב. (כלומר, לא להשתמש בגיאומטריה מורכבת אם אין צורך בה).

בדף ניפוי הבאגים מפורטות דוגמאות לשימוש ב-Export כדי לפתור את השגיאות Computation timed out ו-Too many concurrent aggregations. פרטים נוספים על ייצוא באופן כללי זמינים במסמך הזה.

אם אתם לא צריכים לחתוך את הסרטון, אל תשתמשו ב-clip()

שימוש ב-clip() ללא צורך יאריך את זמן החישוב. מומלץ להימנע משימוש ב-clip() אלא אם הוא נחוץ לניתוח. אם אתם לא בטוחים, אל תיצרו קליפ. דוגמה לשימוש לא הולם בקליפ:

אין לחתוך קלט שלא לצורך!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var belgium = table.filter(ee.Filter.eq('country_na', 'Belgium')).first();

// Do NOT clip unless you need to.
var unnecessaryClip = l8sr
    .select('SR_B4')                          // Good.
    .filterBounds(belgium.geometry())         // Good.
    .filterDate('2019-01-01', '2019-04-01')   // Good.
    .map(function(image) {
      return image.clip(belgium.geometry());  // NO! Bad! Not necessary.
    })
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(),
      scale: 30,
      maxPixels: 1e10,
    });
print(unnecessaryClip);

אפשר לדלג לגמרי על חיתוך של תמונות הקלט, כי האזור מצוין בקריאה ל-reduceRegion():

צריך לציין את האזור של הפלט!

var noClipNeeded = l8sr
    .select('SR_B4')                           // Good.
    .filterBounds(belgium.geometry())          // Good.
    .filterDate('2019-01-01', '2019-12-31') // Good.
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(), // Geometry is specified here.
      scale: 30,
      maxPixels: 1e10,
    });
print(noClipNeeded);

אם חל הזמן הקצוב לחישוב, Export אותו כפי שמתואר בדוגמה הזו.

אם אתם צריכים ליצור קליפים עם קולקציה מורכבת, השתמשו ב-clipToCollection()

אם אתם באמת צריכים לחתוך משהו, והגיאומטריות שבהן אתם רוצים להשתמש לחיתוך נמצאות באוסף, השתמשו ב-clipToCollection():

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME',
                         'Tropical & Subtropical Moist Broadleaf Forests'));
Map.addLayer(complexCollection, {}, 'complexCollection');

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay', false);

אין להשתמש ב-featureCollection.geometry() או ב-featureCollection.union() באוספים גדולים או מורכבים, כי הם עשויים לדרוש יותר זיכרון.

אין להשתמש בקולקציה מורכבת כאזור של מחולל (reducer)

אם אתם צריכים לבצע צמצום מרחבי כך שה-reducer יצבור קלט ממספר אזורים ב-FeatureCollection, אל תספקו את featureCollection.geometry() כקלט geometry ל-reducer. במקום זאת, צריך להשתמש ב-clipToCollection() ובאזור גדול מספיק כדי לכלול את גבולות האוסף. לדוגמה:

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME', 'Tropical & Subtropical Moist Broadleaf Forests'));

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay');

var reduction = clippedTheRightWay.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: ee.Geometry.Rectangle({
    coords: [-179.9, -50, 179.9, 50],  // Almost global.
    geodesic: false
  }),
  scale: 30,
  maxPixels: 1e12
});
print(reduction); // If this times out, export it.

שימוש ב-errorMargin שאינו אפס

בפעולות גיאומטריה שעשויות להיות יקרות, כדאי להשתמש בטווח השגיאה הגדול ביותר האפשרי בהתאם לרמת הדיוק הנדרשת של החישוב. מרווח הטעות מציין את השגיאה המקסימלית (במטרים) שמותר לבצע במהלך פעולות על גיאומטריות (למשל, במהלך הקרנה מחדש). ציון מרווח שגיאה קטן עלול לגרום לצורך לעבות את הגיאומטריות (עם קואורדינטות), ופעולה כזו עשויה לדרוש כמות גדולה של זיכרון. מומלץ לציין מרווח שגיאה גדול ככל האפשר לחישוב:

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');

var complexCollection = ecoregions.limit(10);
Map.centerObject(complexCollection);
Map.addLayer(complexCollection);

var expensiveOps = complexCollection.map(function(f) {
  return f.buffer(10000, 200).bounds(200);
});
Map.addLayer(expensiveOps, {}, 'expensiveOps');

אין להשתמש בסולם קטן באופן מגוחך עם reduceToVectors()

אם רוצים להמיר קובץ רסטר לווקטור, צריך להשתמש בקנה מידה מתאים. ציון סולם קטן מאוד יכול להגדיל משמעותית את עלות החישוב. מגדירים את התצוגה ברזולוציה הגבוהה ביותר שמספקת את רמת הדיוק הנדרשת. לדוגמה, כדי לקבל פוליגונים שמייצגים יבשות ברחבי העולם:

var etopo = ee.Image('NOAA/NGDC/ETOPO1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});
Map.addLayer(almostGlobal, {}, 'almostGlobal');

var vectors = bounds.selfMask().reduceToVectors({
  reducer: ee.Reducer.countEvery(),
  geometry: almostGlobal,
  // Set the scale to the maximum possible given
  // the required precision of the computation.
  scale: 50000,
});
Map.addLayer(vectors, {}, 'vectors');

בדוגמה הקודמת, שימו לב לשימוש בפוליגון לא גיאודטי לצורך הפחתות גלובליות.

אין להשתמש ב-reduceToVectors() עם reduceRegions()

אין להשתמש ב-FeatureCollection שהוחזר על ידי reduceToVectors() כקלט ל-reduceRegions(). במקום זאת, מוסיפים את התדרים שרוצים לצמצם לפני שמפעילים את הפקודה reduceToVectors():

var etopo = ee.Image('NOAA/NGDC/ETOPO1');
var mod11a1 = ee.ImageCollection('MODIS/006/MOD11A1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});

var lst = mod11a1.first().select(0);

var means = bounds.selfMask().addBands(lst).reduceToVectors({
  reducer: ee.Reducer.mean(),
  geometry: almostGlobal,
  scale: 1000,
  maxPixels: 1e10
});
print(means.limit(10));

חשוב לדעת שיש דרכים אחרות לצמצם את מספר הפיקסלים של תמונה אחת בתוך תחומים של תמונה אחרת, כולל reduceConnectedCommponents() ו/או grouping reducers.

שימוש במשתנה fastDistanceTransform() לפעולות של שכונה

בחלק מפעולות הגלול, fastDistanceTransform() עשוי להיות יעיל יותר מ-reduceNeighborhood() או מ-convolve(). לדוגמה, כדי לבצע שחיקה ו/או הרחבה של קלט בינארי:

var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

// Make a simple binary layer from a threshold on elevation.
var mask = aw3d30.select('AVE').gt(300);
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(mask, {}, 'mask');

// Distance in pixel units.
var distance = mask.fastDistanceTransform().sqrt();
// Threshold on distance (three pixels) for a dilation.
var dilation = distance.lt(3);
Map.addLayer(dilation, {}, 'dilation');

// Do the reverse for an erosion.
var notDistance = mask.not().fastDistanceTransform().sqrt();
var erosion = notDistance.gt(3);
Map.addLayer(erosion, {}, 'erosion');

שימוש באופטימיזציות ב-reduceNeighborhood()

אם אתם צריכים לבצע עיבוד נתונים באמצעות convolve ולא ניתן להשתמש ב-fastDistanceTransform(), תוכלו להשתמש באופטימיזציות שמפורטות ב-reduceNeighborhood().

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);

var bands = ['B4', 'B3', 'B2'];

var optimizedConvolution = composite.select(bands).reduceNeighborhood({
  reducer: ee.Reducer.mean(),
  kernel: ee.Kernel.square(3),
  optimization: 'boxcar' // Suitable optimization for mean.
}).rename(bands);

var viz = {bands: bands, min: 0, max: 72};
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(composite, viz, 'composite');
Map.addLayer(optimizedConvolution, viz, 'optimizedConvolution');

לא לבחור יותר מדי נתונים לדגימה

אל תתפתתו להגדיל את גודל מערך הנתונים לאימון ללא צורך. הגדלת כמות נתוני האימון היא אסטרטגיה יעילה ללמידת מכונה בנסיבות מסוימות, אבל היא עלולה גם להגדיל את עלות החישוב בלי שיפור ממשי בדיוק. (במאמר הזה מוסבר מתי כדאי להגדיל את גודל מערך הנתונים של האימון). בדוגמה הבאה מוסבר איך בקשה של יותר מדי נתוני אימון עלולה לגרום לשגיאה המפחידה 'הערך המחושב גדול מדי':

לא כדאי לדגום יותר מדי נתונים!

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// No!  Not necessary.  Don't do this:
labels = labels.map(function(f) { return f.buffer(100000, 1000); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var training = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

var classifier = ee.Classifier.smileCart().train({
  features: training,
  classProperty: 'landcover',
  inputProperties: bands
});
print(classifier.explain()); // Computed value is too large

הגישה הטובה יותר היא להתחיל עם כמות נתונים מתונה ולכוונן את הפרמטרים העל-היפר של הסיווג כדי לקבוע אם אפשר להשיג את רמת הדיוק הרצויה:

כוונון של היפר-פרמטרים

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// Increase the data a little bit, possibly introducing noise.
labels = labels.map(function(f) { return f.buffer(100, 10); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var data = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

// Add a column of uniform random numbers called 'random'.
data = data.randomColumn();

// Partition into training and testing.
var training = data.filter(ee.Filter.lt('random', 0.5));
var testing = data.filter(ee.Filter.gte('random', 0.5));

// Tune the minLeafPopulation parameter.
var minLeafPops = ee.List.sequence(1, 10);

var accuracies = minLeafPops.map(function(p) {
  var classifier = ee.Classifier.smileCart({minLeafPopulation: p})
      .train({
        features: training,
        classProperty: 'landcover',
        inputProperties: bands
      });

  return testing
    .classify(classifier)
    .errorMatrix('landcover', 'classification')
    .accuracy();
});

print(ui.Chart.array.values({
  array: ee.Array(accuracies),
  axis: 0,
  xLabels: minLeafPops
}));

בדוגמה הזו, הסיווג כבר מדויק מאוד, כך שאין צורך לבצע התאמה רבה. מומלץ לבחור את העץ הקטן ביותר האפשרי (כלומר minLeafPopulation הגדול ביותר) שעדיין שומר על הדיוק הנדרש.

Export תוצאות ביניים

נניח שהמטרה שלכם היא לקחת דגימות מתמונה ממוחשבת מורכבת יחסית. בדרך כלל יעיל יותר Export את התמונה toAsset(), לטעון את התמונה המיוצאת ואז לדגום. לדוגמה:

var image = ee.Image('UMD/hansen/global_forest_change_2018_v1_6');
var geometry = ee.Geometry.Polygon(
    [[[-76.64069800085349, 5.511777325802095],
      [-76.64069800085349, -20.483938229362376],
      [-35.15632300085349, -20.483938229362376],
      [-35.15632300085349, 5.511777325802095]]], null, false);
var testRegion = ee.Geometry.Polygon(
    [[[-48.86726050085349, -3.0475996402515717],
      [-48.86726050085349, -3.9248707849303295],
      [-47.46101050085349, -3.9248707849303295],
      [-47.46101050085349, -3.0475996402515717]]], null, false);

// Forest loss in 2016, to stratify a sample.
var loss = image.select('lossyear');
var loss16 = loss.eq(16).rename('loss16');

// Scales and masks Landsat 8 surface reflectance images.
function prepSrL8(image) {
  var qaMask = image.select('QA_PIXEL').bitwiseAnd(parseInt('11111', 2)).eq(0);
  var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
  var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(opticalBands, null, true)
      .addBands(thermalBands, null, true)
      .updateMask(qaMask);
}

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .map(prepSrL8);

// Create two annual cloud-free composites.
var composite1 = collection.filterDate('2015-01-01', '2015-12-31').median();
var composite2 = collection.filterDate('2017-01-01', '2017-12-31').median();

// We want a strtatified sample of this stack.
var stack = composite1.addBands(composite2)
    .float(); // Export the smallest size possible.

// Export the image.  This block is commented because the export is complete.
/*
var link = '0b8023b0af6c1b0ac7b5be649b54db06'
var desc = 'Logistic_regression_stack_' + link;
Export.image.toAsset({
  image: stack,
  description: desc,
  assetId: desc,
  region: geometry,
  scale: 30,
  maxPixels: 1e10
})
*/

// Load the exported image.
var exportedStack = ee.Image(
  'projects/google/Logistic_regression_stack_0b8023b0af6c1b0ac7b5be649b54db06');

// Take a very small sample first, to debug.
var testSample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 1,
  classBand: 'loss16',
  region: testRegion,
  scale: 30,
  geometries: true
});
print(testSample); // Check this in the console.

// Take a large sample.
var sample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 10000,
  classBand: 'loss16',
  region: geometry,
  scale: 30,
});

// Export the large sample...

בדוגמה הזו, שימו לב שהתמונות מיוצאות כ-float. אל תיצאו במדויקות כפולה אלא אם זה הכרחי. חשוב לזכור: כשאתם מייצאים את הקובץ, קישור ל-Code Editor (שמתקבל מיד לפני הייצוא) מוטמע בשם הקובץ כדי שתוכלו לשחזר אותו.

אחרי שהייצוא יושלם, צריך לטעון מחדש את הנכס ולהמשיך ביצירת דגימות ממנו. לתשומת ליבכם: קודם מריצים דגימה קטנה מאוד באזור בדיקה קטן מאוד, לצורך ניפוי באגים. אם הפעולה הזו מתבצעת בהצלחה, צריך לבחור דגימה גדולה יותר ולייצא אותה. בדרך כלל צריך לייצא דגימות גדולות כאלה. לא ניתן לצפות שדוגמאות כאלה יהיו זמינות באופן אינטראקטיבי (למשל דרך print()) או שאפשר יהיה להשתמש בהן (למשל כקלט למסווג) בלי לייצא אותן קודם.

הצטרפות לעומת מסנן מפה

נניח שאתם רוצים לצרף אוספים על סמך זמן, מיקום או מאפיין מטא-נתונים כלשהו. באופן כללי, הדרך היעילה ביותר לעשות זאת היא באמצעות איחוד. בדוגמה הבאה מתבצעת צירוף מרחבי-זמן בין האוספים של Landsat 8 ו-Sentinel-2:

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var joined = ee.Join.saveAll('landsat').apply({
  primary: s2,
  secondary: l8,
  condition: ee.Filter.and(
    ee.Filter.maxDifference({
      difference: 1000 * 60 * 60 * 24, // One day in milliseconds
      leftField: 'system:time_start',
      rightField: 'system:time_start',
    }),
    ee.Filter.intersects({
      leftField: '.geo',
      rightField: '.geo',
    })
  )
});
print(joined);

מומלץ לנסות קודם את השיטה 'צירוף' (Export במקרה הצורך), אבל לפעמים גם השיטה filter() בתוך map() יכולה להיות יעילה, במיוחד עבור אוספים גדולים מאוד.

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var mappedFilter = s2.map(function(image) {
  var date = image.date();
  var landsat = l8
      .filterBounds(image.geometry())
      .filterDate(date.advance(-1, 'day'), date.advance(1, 'day'));
  // Return the input image with matching scenes in a property.
  return image.set({
    landsat: landsat,
    size: landsat.size()
  });
}).filter(ee.Filter.gt('size', 0));
print(mappedFilter);

reduceRegion() לעומת reduceRegions() לעומת לולאת for

קריאה ל-reduceRegions() עם FeatureCollection גדול או מורכב מאוד כקלט עלולה לגרום לשגיאה המפחידה "הערך המחושב גדול מדי". פתרון אפשרי אחד הוא למפות את reduceRegion() מעל FeatureCollection במקום זאת. פתרון אפשרי נוסף הוא להשתמש (אוי ואבוי) בלולאה for. מומלץ מאוד לא לעשות זאת ב-Earth Engine, כפי שמתואר כאן, כאן וכאן, אבל אפשר להטמיע את reduceRegion() בלול של 'ל' כדי לבצע הפחתות גדולות.

נניח שהמטרה שלכם היא לקבל את הממוצע של הפיקסלים (או כל נתון סטטיסטי אחר) בכל מאפיין ב-FeatureCollection לכל תמונה ב-ImageCollection. בדוגמה הבאה מוצגת השוואה בין שלוש הגישות שתיארנו למעלה:

// Table of countries.
var countriesTable = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017");
// Time series of images.
var mod13a1 = ee.ImageCollection("MODIS/006/MOD13A1");

// MODIS vegetation indices (always use the most recent version).
var band = 'NDVI';
var imagery = mod13a1.select(band);

// Option 1: reduceRegions()
var testTable = countriesTable.limit(1); // Do this outside map()s and loops.
var data = imagery.map(function(image) {
  return image.reduceRegions({
    collection: testTable,
    reducer: ee.Reducer.mean(),
    scale: 500
  }).map(function(f) {
    return f.set({
      time: image.date().millis(),
      date: image.date().format()
    });
  });
}).flatten();
print(data.first());

// Option 2: mapped reduceRegion()
var data = countriesTable.map(function(feature) {
  return imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  });
}).flatten();
print(data.first());

// Option 3: for-loop (WATCH OUT!)
var size = countriesTable.size();
// print(size); // 312
var countriesList = countriesTable.toList(1); // Adjust size.
var data = ee.FeatureCollection([]); // Empty table.
for (var j=0; j<1; j++) { // Adjust size.
  var feature = ee.Feature(countriesList.get(j));
  // Convert ImageCollection > FeatureCollection
  var fc = ee.FeatureCollection(imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  }));
  data = data.merge(fc);
}
print(data.first());

שימו לב שהאובייקט first() מכל קולקציה מודפס למטרות ניפוי באגים. לא צפוי שהתוצאה המלאה תהיה זמינה באופן אינטראקטיבי: תצטרכו Export. חשוב גם לזכור שצריך להשתמש בלולאות for בזהירות רבה, ורק כמוצא אחרון. לבסוף, כדי להשתמש בלול for צריך לקבל באופן ידני את הגודל של אוסף הקלט ולהטמיע אותו באופן קבוע במיקומים המתאימים. אם משהו מזה לא ברור לכם, אל תשתמשו בלול for.

שימוש בהבדלים קדימה לשכנים בזמן

נניח שיש לכם ImageCollection ממוין לפי זמן (כלומר, סדרה כרונולוגית) ואתם רוצים להשוות כל תמונה לתמונה הקודמת (או הבאה). במקום להשתמש ב-iterate() למטרה הזו, יכול להיות שיהיה יעיל יותר להשתמש בהבדל קדימה שמבוסס על מערך. בדוגמה הבאה נעשה שימוש בשיטה הזו כדי לבטל כפילויות באוסף Sentinel-2, כאשר כפילויות מוגדרות כתמונות עם אותו יום בשנה:

var sentinel2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED');
var sf = ee.Geometry.Point([-122.47555371521855, 37.76884708376152]);
var s2 = sentinel2
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-12-31');

var withDoys = s2.map(function(image) {
  var ndvi = image.normalizedDifference(['B4', 'B8']).rename('ndvi');
  var date = image.date();
  var doy = date.getRelative('day', 'year');
  var time = image.metadata('system:time_start');
  var doyImage = ee.Image(doy)
      .rename('doy')
      .int();
  return ndvi.addBands(doyImage).addBands(time)
      .clip(image.geometry()); // Appropriate use of clip.
});

var array = withDoys.toArray();
var timeAxis = 0;
var bandAxis = 1;

var dedupe = function(array) {
  var time = array.arraySlice(bandAxis, -1);
  var sorted = array.arraySort(time);
  var doy = sorted.arraySlice(bandAxis, -2, -1);
  var left = doy.arraySlice(timeAxis, 1);
  var right = doy.arraySlice(timeAxis, 0, -1);
  var mask = ee.Image(ee.Array([[1]]))
      .arrayCat(left.neq(right), timeAxis);
  return array.arrayMask(mask);
};

var deduped = dedupe(array);

// Inspect these outputs to confirm that duplicates have been removed.
print(array.reduceRegion('first', sf, 10));
print(deduped.reduceRegion('first', sf, 10));

בודקים את האוספים המודפסים כדי לוודא שהעותקים הכפולים הוסרו.