- KFS × Navera
- IoT & AI for Smart Agriculture
- السيشن 15
أساسيات تعلم الآلة في الزراعة
Machine Learning Basics in Agriculture
الانحدار (Regression) للتنبؤ برطوبة التربة والبخر-نتح، والتصنيف (Classification) لحالة التربة (جاف/رطب/مشبع)، مع تقسيم زمني صحيح للتدريب والاختبار، ومقاييس MAE و RMSE و Accuracy — والأهم: المقارنة دائماً بنموذج مرجعي بسيط (Baseline). نبني نموذجاً يتنبأ برطوبة التربة بعد 6 ساعات.
نظرة عامة على السيشن
بنهاية السيشن ستكون قادراً على
- تفرّق بين Supervised learning بنوعيه: Regression (رقم) و Classification (فئة).
- تبني Features من السلاسل الزمنية (Lags، Rolling، الساعة) و Target مزاح للمستقبل.
- تقسم البيانات زمنياً (تدريب 2023 / اختبار 2024) وتفهم لماذا لا نخلط عشوائياً.
- تقيم بـ MAE / RMSE / Accuracy وتقارن دائماً بـ Baseline.
- تفهم نموذج الميزان المائي FAO-56 كأساس زراعي لعتبات الري.
التطبيق في مزرعة الكلية
التنبؤ برطوبة تربة الحقل بعد 6 ساعات وبالبخر-نتح للغد من بيانات الطقس الحقيقية لموقع المزرعة، وتحويل التوقع إلى احتياج مائي بالمتر المكعب للفدان، ثم تجربة نفس الفكرة على بيانات عقدة فريقك.
المُخرج (Deliverable)
نموذج أولي مُدرب وقابل للتقييم (soil_moisture_6h_rf.joblib) مع جدول مقارنة بالـ Baseline، وتفسير زراعي للنتائج.
خطة السيشن (180 دقيقة)
- 0:00 مراجعة مراجعة: البيانات النظيفة 10 د
- 0:10 شرح ما هو تعلم الآلة؟ انحدار وتصنيف، تدريب/اختبار، Overfitting، المقاييس 30 د
- 0:40 محاكاة الميزان المائي FAO-56: كيف نعرف متى نروي؟ + محاكي المقاييس 25 د
- 1:05 استراحة استراحة 10 د
- 1:15 تطبيق Lab: نموذج انحدار لرطوبة التربة بعد 6 ساعات 35 د
- 1:50 مشروع المشروع الجاهز: s15 — ثلاث تجارب ونتائجها الحقيقية 30 د
- 2:20 ميداني تطبيقي: نموذج على بيانات عقدة فريقك 30 د
- 2:50 تقييم اختبار سريع + الواجب 10 د
الأدوات والمكونات
قاموس المصطلحات (Glossary)
الحاسوب يتعلم العلاقة من أمثلة بدلاً من قواعد نكتبها يدوياً.
أمثلة لها إجابة معروفة (Label).
متغير يستخدمه النموذج: الحرارة، رطوبة الأمس.
ما نريد التنبؤ به: الرطوبة بعد 6 ساعات.
التنبؤ برقم مستمر.
التنبؤ بفئة (dry / moist / wet).
ندرّب على جزء ونقيّم على جزء لم يره النموذج.
أبسط تنبؤ ممكن (القيمة الحالية) — يجب التفوق عليه.
متوسط |الحقيقي − المتوقع| بنفس الوحدة.
يعاقب الأخطاء الكبيرة أكثر من MAE.
مجموعة أشجار قرار تصوّت — قوي ولا يحتاج تطبيعاً.
النموذج يحفظ بيانات التدريب ويفشل على الجديد.
مفاهيم تعلم الآلة
30 دقيقة| السؤال الزراعي | النوع | Target | مقياس التقييم |
|---|---|---|---|
| كم ستكون رطوبة التربة بعد 6 ساعات؟ | Regression | VWC (m³/m³) | MAE, RMSE |
| كم مم سيفقد المحصول غداً؟ | Regression | ET0 الغد | MAE (mm) |
| التربة الآن: جافة/رطبة/مشبعة؟ | Classification (3 فئات) | soil_state | Accuracy + Report |
| هل نحتاج ري خلال 24 ساعة؟ | Classification (ثنائي) | irrigate 0/1 | F1, Precision, Recall (السيشن 16) |
التقسيم الزمني — قاعدة لا تُكسر
في السلاسل الزمنية لا نخلط البيانات عشوائياً. لو دربنا على يوم 10 يونيو واختبرنا على 9 يونيو، فالنموذج «يعرف» الطقس حول يوم الاختبار — نتيجة ممتازة مزيفة. الصحيح: ندرّب على الماضي (2023) ونختبر على المستقبل (2024) كما سيحدث فعلاً في المزرعة.
Baseline: «الغد مثل اليوم»
رطوبة التربة تتغير ببطء، فأبسط تنبؤ «ستبقى كما هي الآن» يكون جيداً جداً غالباً. أي نموذج لا يتفوق عليه لا قيمة له مهما بدت أرقامه جميلة.
الميزان المائي FAO-56
25 دقيقةمنظمة الأغذية والزراعة (FAO) تصف التربة كـ «دلو»: الماء المتاح الكلي TAW بين السعة الحقلية ونقطة الذبول في منطقة الجذور. النبات يستهلك من الدلو بمعدل ETc = ET0 × Kc، والمطر والري يعيدان ملأه. نروي عندما يتجاوز النقص RAW (الماء سهل الاستخدام) قبل أن يبدأ الإجهاد.
| المعامل | القيمة | المعنى |
|---|---|---|
| θFC | 0.36 m³/m³ | السعة الحقلية |
| θWP | 0.20 m³/m³ | نقطة الذبول |
| Zr | 0.40 م | عمق الجذور الفعال |
| TAW | 1000 × (0.36−0.20) × 0.4 = 64 مم | الماء المتاح الكلي |
| p | 0.5 | نسبة الاستنزاف المسموحة |
| RAW | 32 مم ⇒ نقطة إعادة الري = 0.28 m³/m³ | نروي عندها |
| Kc | 1.05 | معامل المحصول (خضر منتصف الموسم) |
import sys; sys.path.insert(0, "projects/ai")
from common import load_hourly, simulate_irrigated_field, RAW_MM, REFILL_POINT
field = simulate_irrigated_field(load_hourly()) # real weather -> simulated irrigated field
year = field.loc["2024"]
print("RAW =", RAW_MM, "mm | refill point =", round(REFILL_POINT, 3), "m3/m3")
print("irrigation events 2024:", int((year.irrigation_mm > 0).sum()))
print("water applied 2024:", round(year.irrigation_mm.sum()), "mm =", round(year.irrigation_mm.sum() * 4.2), "m3/feddan")
monthly = year.irrigation_mm.resample("MS").agg(["count", "sum"])
monthly["count"] = year.irrigation_mm.gt(0).resample("MS").sum()
print(monthly.round(0).T)
رطوبة التربة هنا محاكاة (نموذج FAO-56) لحقل مروي مدفوعة بطقس حقيقي — لأن بيانات Open-Meteo لخلية غير مروية لا تحتوي أحداث ري. على بياناتكم الحقيقية من العقد تستخدمون القراءات الفعلية.
Lab: نموذج رطوبة بعد 6 ساعات
35 دقيقةfrom sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error
H = 6
X = pd.DataFrame(index=field.index)
X["vwc_now"] = field["field_vwc"]
for lag in (1, 3, 6, 12):
X[f"vwc_lag{lag}"] = field["field_vwc"].shift(lag) # the past is allowed
X["temp"] = field["air_temp_c"]
X["radiation"] = field["solar_radiation_wm2"]
X["et0_6h"] = field["et0_mm"].rolling(6).sum()
X["hour_sin"] = np.sin(2 * np.pi * field.index.hour / 24)
y = field["field_vwc"].shift(-H).rename("target") # the FUTURE is the target
data = X.join(y).dropna()
# irrigation is a management decision, not weather -> drop windows that contain one
irrig_soon = field["irrigation_mm"][::-1].rolling(H, min_periods=1).max()[::-1].shift(-1).reindex(data.index) > 0
data = data[~irrig_soon]
train, test = data.loc[:"2023-12-31"], data.loc["2024-01-01":]
cols = [c for c in data.columns if c != "target"]
def report(name, pred):
mae = mean_absolute_error(test.target, pred)
rmse = np.sqrt(mean_squared_error(test.target, pred))
print(f"{name:12s} MAE={mae:.4f} RMSE={rmse:.4f}")
report("baseline", test["vwc_now"])
lin = LinearRegression().fit(train[cols], train.target)
report("linear", lin.predict(test[cols]))
rf = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, n_jobs=-1, random_state=0).fit(train[cols], train.target)
report("forest", rf.predict(test[cols]))
فسّر النتيجة
خطأ 0.003 m³/m³ في جذور عمقها 40 سم يساوي كم مليمتر ماء؟ (0.003 × 400 مم = 1.2 مم) — هل هذه الدقة كافية لقرار ري؟ قارنها بـ RAW = 32 مم.
المشروع الجاهز: ثلاث تجارب
30 دقيقةs15_predict_soil_moisture.py
A) رطوبة الحقل بعد 6 ساعات. B) ET0 الغد من طقس اليوم (بيانات حقيقية بالكامل) وتحويله لمتر مكعب للفدان. C) تصنيف حالة التربة بدون حساس التربة (من الطقس وسجل الري فقط). هذه مخرجات التشغيل الفعلية:
cd projects/ai
python s15_predict_soil_moisture.py
المخرجات الفعلية عند التشغيل (Real output)
Part A - field VWC 6 h ahead | train=8436 test=8454
Persistence (baseline) MAE=0.0051 RMSE=0.0065 m3/m3
LinearRegression MAE=0.0034 RMSE=0.0043 m3/m3
RandomForest MAE=0.0029 RMSE=0.0037 m3/m3
Part B - tomorrow's ET0 (mm/day), real weather
Persistence (baseline) MAE=0.417 mm
LinearRegression MAE=0.396 mm RMSE=0.527 mm
RandomForest MAE=0.410 mm RMSE=0.546 mm
Tomorrow's crop water need (Kc=1.05): 2.30 mm -> 9.7 m3 per feddan
Part C - soil state WITHOUT the soil sensor (weather + irrigation log only)
Accuracy: 0.911
precision recall f1-score support
dry 0.70 0.58 0.64 879
moist 0.94 0.96 0.95 7209
wet 0.86 0.85 0.86 696| التجربة | النتيجة | الدرس |
|---|---|---|
| A — رطوبة 6 ساعات | Random Forest أفضل من Baseline بـ 43% | النموذج تعلم معدل الجفاف من الحرارة والإشعاع |
| B — ET0 الغد | Linear أفضل بـ 5% فقط | طقس الغد لا يُعرف جيداً من طقس اليوم — نحتاج بيانات التنبؤ الجوي (منصتنا تجلبها من Open-Meteo في السيشن 17) |
| C — حالة التربة | Accuracy 0.91 لكن Recall للجاف 0.58 | الدقة الكلية تخدع! الفئة الأهم زراعياً (جاف) هي الأضعف ⇒ السيشن 16 |
"""Session 15 - Machine Learning basics in agriculture.
Part A Regression: predict the field soil moisture 6 hours ahead (Linear vs Random Forest, MAE / RMSE)
Part B Regression: predict TOMORROW's reference evapotranspiration ET0 from today's real weather
Part C Classification: soil state (dry / moist / wet) (Accuracy + report)
Data: real KFS-farm weather 2023-2024 (Open-Meteo) + simulated irrigated field (FAO-56, see common.py)
Time-based split: train = 2023, test = 2024 (never shuffle time series!)
"""
import joblib
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import accuracy_score, classification_report, mean_absolute_error, mean_squared_error
from common import KC, OUT, load_hourly, simulate_irrigated_field, soil_state
HORIZON = 6
df = simulate_irrigated_field(load_hourly())
# ---------------- Part A: field VWC in 6 h
feat = pd.DataFrame(index=df.index)
feat["vwc_now"] = df["field_vwc"]
for lag in (1, 3, 6, 12):
feat[f"vwc_lag{lag}"] = df["field_vwc"].shift(lag)
feat["vwc_change_6h"] = df["field_vwc"].diff(6)
feat["temp"] = df["air_temp_c"]
feat["humidity"] = df["air_humidity_pct"]
feat["radiation"] = df["solar_radiation_wm2"]
feat["wind"] = df["wind_speed_kmh"]
feat["et0_next6h_proxy"] = df["et0_mm"].rolling(6).sum() # last 6 h demand
feat["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24)
feat["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24)
feat["target"] = df["field_vwc"].shift(-HORIZON)
feat = feat.dropna()
# Irrigation events are planned by the farm manager, not by the weather -> exclude windows containing one
irrigated_soon = df["irrigation_mm"][::-1].rolling(HORIZON, min_periods=1).max()[::-1].shift(-1).reindex(feat.index) > 0
feat = feat[~irrigated_soon]
train, test = feat.loc[:"2023-12-31"], feat.loc["2024-01-01":]
X_cols = [c for c in feat.columns if c != "target"]
print(f"Part A - field VWC {HORIZON} h ahead | train={len(train)} test={len(test)}")
models = {"Persistence (baseline)": None, "LinearRegression": LinearRegression(),
"RandomForest": RandomForestRegressor(n_estimators=200, min_samples_leaf=5, n_jobs=-1, random_state=0)}
for name, model in models.items():
if model is None:
pred = test["vwc_now"]
else:
model.fit(train[X_cols], train["target"])
pred = model.predict(test[X_cols])
print(f" {name:24s} MAE={mean_absolute_error(test.target, pred):.4f} "
f"RMSE={np.sqrt(mean_squared_error(test.target, pred)):.4f} m3/m3")
joblib.dump({"model": models["RandomForest"], "features": X_cols, "horizon_h": HORIZON},
OUT / "soil_moisture_6h_rf.joblib", compress=3)
# ---------------- Part B: tomorrow's ET0 (real data only)
daily = df.resample("D").agg({"et0_mm": "sum", "air_temp_c": ["mean", "max"], "air_humidity_pct": "mean",
"solar_radiation_wm2": "mean", "wind_speed_kmh": "mean"})
daily.columns = ["et0", "t_mean", "t_max", "rh", "rad", "wind"]
dfe = daily.add_suffix("_today")
dfe["et0_3d"] = daily["et0"].rolling(3).mean()
dfe["doy_sin"] = np.sin(2 * np.pi * daily.index.dayofyear / 365)
dfe["doy_cos"] = np.cos(2 * np.pi * daily.index.dayofyear / 365)
dfe["target"] = daily["et0"].shift(-1)
dfe = dfe.dropna()
e_tr, e_te = dfe.loc[:"2023-12-31"], dfe.loc["2024-01-01":]
e_cols = [c for c in dfe.columns if c != "target"]
print("\nPart B - tomorrow's ET0 (mm/day), real weather")
print(f" {'Persistence (baseline)':24s} MAE={mean_absolute_error(e_te.target, e_te.et0_today):.3f} mm")
for name, m in {"LinearRegression": LinearRegression(),
"RandomForest": RandomForestRegressor(n_estimators=300, min_samples_leaf=3, random_state=0)}.items():
m.fit(e_tr[e_cols], e_tr.target)
p_ = m.predict(e_te[e_cols])
print(f" {name:24s} MAE={mean_absolute_error(e_te.target, p_):.3f} mm "
f"RMSE={np.sqrt(mean_squared_error(e_te.target, p_)):.3f} mm")
joblib.dump({"model": m, "features": e_cols}, OUT / "et0_next_day_rf.joblib", compress=3)
etc = float(p_[-1]) * KC
print(f" Tomorrow's crop water need (Kc={KC}): {etc:.2f} mm -> {etc * 4.2:.1f} m3 per feddan")
# ---------------- Part C: soil state classification
states = df["field_vwc"].apply(soil_state)
Xc = pd.DataFrame({"depletion_proxy_et0_72h": df["et0_mm"].rolling(72).sum(),
"hours_since_irrigation": df.groupby((df["irrigation_mm"] > 0).cumsum()).cumcount(),
"temp": df["air_temp_c"], "humidity": df["air_humidity_pct"],
"radiation": df["solar_radiation_wm2"]}).dropna()
yc = states.loc[Xc.index]
clf = RandomForestClassifier(n_estimators=200, min_samples_leaf=5, random_state=0, n_jobs=-1)
clf.fit(Xc.loc[:"2023-12-31"], yc.loc[:"2023-12-31"])
pc = clf.predict(Xc.loc["2024-01-01":])
print("\nPart C - soil state WITHOUT the soil sensor (weather + irrigation log only)")
print(" Accuracy:", round(accuracy_score(yc.loc["2024-01-01":], pc), 3))
print(classification_report(yc.loc["2024-01-01":], pc, zero_division=0))
تطبيقي: نموذج عقدة فريقك
30 دقيقةتنبؤ برطوبة تربة عقدتكم بعد 6 ساعات
بيانات عقدة فريقكم (node_log_hourly.csv من السيشن 14 أو farm.db)
- البيانات
استخدم الملف الساعي للعقدة بعد التنظيف.
- النموذج
طبّق نفس الخطوات (Lags + Target مزاح 6 ساعات) مع تقسيم زمني: أول 70% تدريب.
- المقارنة
جدول: Baseline / Linear / Forest.
- الحقل
اذهب لعقدتك وقارن قراءتها الحالية بتوقع النموذج قبل 6 ساعات. سجل الفرق.
| النموذج | MAE (%) | RMSE (%) | أفضل من Baseline؟ | تعليق |
|---|---|---|---|---|
hourly = pd.read_csv("projects/ai/outputs/node_log_hourly.csv", parse_dates=["timestamp"])
s = hourly[hourly.node_id == "field_A"].set_index("timestamp")["soil_moisture_pct"].interpolate(limit=2)
d = pd.DataFrame({"now": s, "lag1": s.shift(1), "lag3": s.shift(3), "lag24": s.shift(24),
"hour": s.index.hour, "target": s.shift(-6)}).dropna()
cut = int(len(d) * 0.7)
tr, te = d.iloc[:cut], d.iloc[cut:]
f = ["now", "lag1", "lag3", "lag24", "hour"]
m = RandomForestRegressor(n_estimators=200, min_samples_leaf=3, random_state=0).fit(tr[f], tr.target)
print("baseline MAE:", round(mean_absolute_error(te.target, te.now), 3), "%")
print("forest MAE:", round(mean_absolute_error(te.target, m.predict(te[f])), 3), "%")
اختبر فهمك
6 أسئلة · نتيجة فوريةالواجب والتسليم
الواجب الأسبوعي (Weekly Assignment)
نفذ تطبيق عقدة فريقك وقدم جدول المقارنة مع تفسير زراعي (5 أسطر).
عدّل KC في common.py إلى 1.2 (ذرة منتصف الموسم): كم حدث ري وكم م³/فدان في 2024؟
غيّر HORIZON إلى 24 ساعة في Part A: ماذا يحدث للخطأ ولتفوق النموذج على الـ Baseline؟ لماذا؟
قائمة التسليم (Submission Checklist)
مراجع ومصادر
استعد للسيشن القادمة
السيشن القادمة نجيب بدقة عن سؤال «هل نروي؟»: البيانات غير المتوازنة، اختيار الميزات، و Cross-validation. حمّل Crop_recommendation.csv من Kaggle (حساب مجاني) لمختبر إضافي.