חיזוי מחירי בתים באמצעות למידת מכונה ומודל מרובה משתנים
אחרי שבמדריך הקודם פיתחנו מודל רגרסיה קווית באמצעות TensorFlow על משתנה אחד בלבד (חיזוי מחירי דירות בהינתן השטח) במדריך הנוכחי נמשיך באותו נושא ונלמד את המכונה לחזות את מחירי הדירות על סמך מספר משתנים (שטח דירה, מספר חדרי השינה ומספר חדרי שירותים). חיזוי על סמך מספר משתנים מכונה ניתוח רב משתנים (multivariate analysis).
המשתנים במדריך שישמשו לאנליזה מתחלקים לשניים, מספריים וקטגוריים. המטרה שלנו היא לפתח מודל שלתוכו נזין את הנתונים של הדירה. שטח, מספר חדרי השינה, סוג הדירה (מגורים, פרטית או רב-משפחתית) ונקבל הערכה של שווי הנכס.
לצורך פיתוח המודל נשתמש ב-TensorFlow 2 שהיא ספרייה של למידת מכונה שפותחה על ידי גוגל, ומשמשת, בין השאר, את מנוע החיפוש הטוב ביותר באינטרנט.
לחץ כאן כדי להוריד קובץ csv של מסד הנתונים
לחץ כאן כדי להוריד את קובץ הקוד המלא שנפתח במדריך
* המדריך הנוכחי מבוסס על מדריך קודם מודל רגרסיה לינארית באמצעות TensorFlow 2 על משתנה אחד בלבד שבו הסברתי כיצד לבנות מודל של למידת מכונה. אם לא קראתם את המדריך הקודם ואין לכם מספיק ידע בתחום, רצוי שתתחילו מקריאת המדריך הקודם שמניח את היסודות לנוכחי.
ייבוא תלויות וספריות
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
- ספריית Numpy מאפשרת לעבוד עם מערכים רב-ממדיים, ומספקת פונקציות לביצוע פעולות של אלגברה לינארית הנדרשות לפתרון הבעיות המתמטיות בתהליך הלמידה.
- Pandas משמשת לסידור ולסינון מידע בדומה לגיליון אקסל.
- Matplotlib משמשת להצגת מידע באמצעות גרפים ותרשימים.
TensorFlow 2 היא הספרייה בה נשתמש ללמידת מכונה. אני משתמש בגרסה 2.2 מכיוון שהיא העדכנית ביותר בזמן עריכת המדריך.
נתקין את הגרסה שמעניינת אותנו:
!pip install tensorflow==2.2.0
את המדריך פתחתי בסביבת Colab ומשמעות סימן הקריאה (!) בתחילת השורה הוא שימוש בטרמינל המובנה של סביבת הפיתוח.
נייבא:
import tensorflow as tf
נוודא שייבאנו את הגרסה שבה אנו מעוניינים:
print(tf.__version__)
2.2.0
ייבוא מסד הנתונים
מסד הנתונים המשמש במדריך מסכם את מחירם של בתים שנמכרו בסקרמנטו. תוכלו להוריד אותו מכאן.
df = pd.read_csv('./sacramentorealestatetransactions.csv',usecols=['beds','baths','sq__ft','type','price'])
סקירת הנתונים וניקוי דוגמאות חריגות
קודם כל, נסקור את מסד הנתונים כדי לנסות לחוש אותו ולנקות ממנו דוגמאות חריגות.
df.shape
(985, 5)
התוצאה היא חמישה טורים ו-985 שורות.
התוצאה היא שני טורים ו-985 שורות.
df.info()
RangeIndex: 985 entries, 0 to 984 Data columns (total 5 columns): beds 985 non-null int64 baths 985 non-null int64 sq__ft 985 non-null int64 type 985 non-null object price 985 non-null int64 dtypes: int64(4), object(1) memory usage: 38.6+ KB
כל הרשומות מלאות. לא חסר מידע.
df.describe()
beds | baths | sq__ft | price | |
---|---|---|---|---|
count | 985.000000 | 985.000000 | 985.000000 | 985.000000 |
mean | 2.911675 | 1.776650 | 1314.916751 | 234144.263959 |
std | 1.307932 | 0.895371 | 853.048243 | 138365.839085 |
min | 0.000000 | 0.000000 | 0.000000 | 1551.000000 |
25% | 2.000000 | 1.000000 | 952.000000 | 145000.000000 |
50% | 3.000000 | 2.000000 | 1304.000000 | 213750.000000 |
75% | 4.000000 | 2.000000 | 1718.000000 | 300000.000000 |
max | 8.000000 | 5.000000 | 5822.000000 | 884790.000000 |
לפי ערכי המינימום, אפשר לראות שחלק מהעמודות כוללות ערכי 0. לדוגמה, מספר חדרים אפסי או שטח אפסי. זה עלול לפגוע בתוצאות האנליזה. ננקה את הנתונים.
ניקוי המידע
ננקה את מסד הנתונים שלנו מכל הדוגמאות שבהם השטח נמוך מ-10 וגם מדוגמאות שבהם מספר חדרי השינה או השירותים אפסי.
filtered_data = df[(df.sq__ft > 10) & (df.beds > 0) & (df.baths > 0)]
מעניין מה מידת הקורלציה בין שטח הבית לבין יתר המשתנים.
filtered_data.corr()['sq__ft'].sort_values()
price 0.693708 beds 0.695710 baths 0.724631 sq__ft 1.000000 Name: sq__ft, dtype: float64
.שטח הבית נמצא בקורלציה חיובית וחזקה למדי עם המחיר וגם עם מספר החדרים
האם ניתן לאתר דוגמאות חריגות נוספות?
def plot_ft_vs_price():
plt.plot(filtered_data['sq__ft'], filtered_data['price'], 'o')
plt.ylabel('Price')
plt.xlabel('Square foot')
plt.title('Price vs square foot')
plot_ft_vs_price()
אפשר לראות דוגמה חריגה כאשר השטח גדול מ-5000. נסיר גם את הדוגמה הזו.
filtered_data = filtered_data[(filtered_data.sq__ft < 5000)]
קידוד המשתנים הקטגוריים
שימוש במשתנה קטגורי (שמי) מציב בעיה מפני שמחשבים צריכים מספרים כדי לעבוד איתם. גישה אחת לפתרון הבעיה היא למפות כל אחת מהקטגוריות למספר. הבעיה עם הגישה הזו שמודלים של למידת מכונה מניחים שיש למספרים ערך כמותי. לדוגמה, אם אנחנו נקודד את השכונות למספרים. שכונה א תקבל את הערך 1, שכונה ב את הערך 2 ושכונה ג את הערך 3. המודל עלול להסיק ששכונה א + ב שוות לשכונה ג. כדי למנוע את הבעיה משתמשים בקידוד one hot encoding שהופכת את המשתנה השמי למערך של 0 ו-1 שבו כל המשתנים מקבלים 0 לבד מאחד שמקבל 1. כל קטגוריה מקבלת 1 במקום שונה במערך.
בוא נראה איך עושים את זה, ובסוף הדוגמה תוכלו לראות בעצמכם איך נראים משתנים קטגוריים אחרי קידוד בשיטת one hot encoding.
נעזר ב-numpy כדי למצות את שמות הקטגוריות של סוגי הדירות.
type_names = np.unique(np.array(filtered_data.type))
type_names
array(['Condo', 'Multi-Family', 'Residential'], dtype=object)
3 שמות ל-3 סוגים.
נשתמש ב-pandas כדי לקודד בשיטת one-hot encoding, המתאימה לקידוד נתונים קטגוריים.
t_dummies = pd.get_dummies(filtered_data.type, dummy_na=False)
נציץ במספר דוגמאות אקראיות כדי לראות את הקידוד בפעולה:
t_dummies.tail(3)
Condo | Multi-Family | Residential | |
---|---|---|---|
979 | 0 | 0 | 1 |
297 | 0 | 0 | 1 |
344 | 1 | 0 | 0 |
- דוגמאות 979 ו-297 שייכות לקטגוריה Residential
- דוגמה 344 שייכת לקטגוריה Condo.
נסיר את העמודה type כי קודדנו אותה באמצעות one-hot encoding ואנחנו לא מעוניינים שעמודה שמית תפריע למודל שלנו שיודע לעבוד עם נתונים מספריים בלבד.
type = filtered_data.pop('type')
נאחד את כל העמודות למסד נתונים אחד שנקרא לו df_united.
df_united = pd.concat([filtered_data, t_dummies], axis=1)
נציץ בזנבו של מסד הנתונים המאוחד כדי לוודא שהאיחוד בין העמודות נעשה כהלכה. אם האינדקסים אינם מתואמים בין העמודות השונות נראה ערכים חסרים בזנב.
df_united.tail(3)
beds | baths | sq__ft | price | Condo | Multi-Family | Residential | |
---|---|---|---|---|---|---|---|
982 | 3 | 2 | 1216 | 235000 | 0 | 0 | 1 |
983 | 4 | 2 | 1685 | 235301 | 0 | 0 | 1 |
984 | 3 | 2 | 1362 | 235738 | 0 | 0 | 1 |
אין נקודות מידע חסרות. נראה שתהליך האיחוד עבר בשלום.
נורמליזציה
עדיין יש לנו בעיה אחת משמעותית והיא שהנתונים שלנו אינם על אותה הסקלה כי השטח נמדד ביחידות של עשרות ואף מאות בעוד החדרים נמדדים ביחידות בודדות. כדי לפתור את הבעיה ננרמל את הנתונים כך שכל אחד מהפיצ'רים יהיה בדיוק באותה הסקלה עם ממוצע 0 וסטיית תקן 1.
אנחנו מנרמלים רק את העמודות שיהוו את הקלט (עמודות ה-x) אבל לא את הפלט שבמקרה שלנו הוא עמודת המחיר. נסיר את עמודת המחיר ממסד הנתונים.
price = filtered_data.pop('price')
את כל יתר העמודות, מספריות וקטגוריות, ננרמל באמצעות הפונקציה הבאה:
def normalize(x):
return (x.values-x.mean()) / x.std()
כל אחד מהפיצ'רים, כל אחת מהעמודות תעבור את אותו תהליך של נורמליזציה.
for col in df_united:
df_united[col] = normalize(df_united[col])
נבחן את מה שעשינו.
df_united.describe()
beds | baths | sq__ft | Condo | Multi-Family | Residential | |
---|---|---|---|---|---|---|
980 | 0.885774 | 1.563404 | 1.072031 | -0.233255 | -0.127397 | 0.269203 |
981 | -0.287083 | 0.062758 | -0.168270 | -0.233255 | -0.127397 | 0.269203 |
982 | -0.287083 | 0.062758 | -0.571407 | -0.233255 | -0.127397 | 0.269203 |
983 | 0.885774 | 0.062758 | 0.153003 | -0.233255 | -0.127397 | 0.269203 |
984 | -0.287083 | 0.062758 | -0.345898 | -0.233255 | -0.127397 | 0.269203 |
אכן. בכל העמודות סטיית התקן היא 1, והממוצע שואף לאפס.
הפרדת הנתונים לסט אימון ומבחן
הנתונים הבלתי תלויים הם מספר חדרי השינה, מספר חדרי אמבטיה, השטח וסוג הדירה. נגדיר אותם כ-x.
x = df_united.values
מכיוון שאנו מנסים לצפות את המחיר אז הוא יהווה את ה-y - המשתנה התלוי שלנו.
y = price.values
נפריד את סט הנתונים למסד נתוני אימון ומבחן.
# Split the data to test and train groups
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.20, random_state=42)
אימון המודל
את המודל נפתח באמצעות TensorFlow 2.
נייבא את החבילות:
# Imports
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
נבנה את המודל:
model = Sequential()
# The model receives 6 inputs and outputs a single value, price
model.add(Dense(32, activation='relu', input_shape=(6,)))
model.add(Dense(32, activation='relu'))
model.add(Dense(1))
- אנחנו מזינים את המודל ב-6 קלטים (שטח, מספר חדרים, מספר שירותים ושלוש סוגי דירות) ומצפים לקבל פלט יחיד (מחיר), וזה מה שמכתיב את הארכיטקטורה של המודל.
- השכבות הם צפופות dense, וכוללות 32 נוירונים בשתי השכבות הראשונות
- בין שכבת הקלט ושכבת הפלט ישנה שכבה חבויה אחת.
- פונקצית האקטיבציה היא relu שהיא הבחירה ברירת המחדל בעולם למידת המכונה.
נתאר את המודל:
model.summary()
Model: "sequential" ___________________________________ Layer (type) Output Shape Param =================================== dense (Dense) (None, 32) 224 ___________________________________ dense_1 (Dense) (None, 32) 1056 ___________________________________ dense_2 (Dense) (None, 1) 33 =================================== Total params: 1,313 Trainable params: 1,313 Non-trainable params: 0
נקמפל את המודל:
# Compile the model
# Adam optimizer for the learning rate
# In preliminary trials I found the learning rate of 10 to be the best for
# the task at hand
model.compile(Adam(lr=10), 'mean_squared_error')
האופטימיזציה נעשית באמצעות ADAM שהוא ברירת המחדל, וקצב הלמידה הוא 10 כי מצאתי בניסוי מקדים שזהו קצב הלמידה שנותן את התוצאות הטובות ביותר.
הפונקציה הבאה תפסיק את תהליך הלמידה כאשר המודל מגיע לרווייה.
# The EarlyStopping callback stops the learning process if the loss doesn't improve
es = EarlyStopping(monitor='val_loss',
min_delta=0,
patience=10,
verbose=1,
mode='auto',
restore_best_weights=True)
את הסבלנות קבעתי על 10 epochs שבמהלכם המודל ימשיך לבדוק האם הגיע לאופטימום . בכל מקרה, האלגוריתם יבחר את ה-epoch האופטימלי וממנו הוא ייקח את המשקלים.
נריץ את המודל:
model.fit(x_train,
y_train,
batch_size=32,
epochs=10000,
validation_data=(x_test,y_test),
callbacks=[es])
תמונת האילוסטרציה הבאה מדגימה כיצד נראה תהליך האימון. במציאות, המודל שלי רץ במשך 1056 epochs.
הערכת המודל
יש לנו מודל. קוראים לו model, אבל מה הוא אומר לנו על הנתונים? עד כמה הוא מצליח לתאר בצורה טובה את סט הנתונים? האם נצליח להפיק ממנו תחזיות מספקות לגבי סט נתוני המבחן?
קודם כל, כדי לקבל תחושה נחשב את המחיר הצפוי עבור דירה אחת מסוימת בסט נתוני המבחן. זו רשומה שמספרה הסידורי 42.
# Pick a single sample and predict
y_pred0 = model.predict(np.array([x_test[42]]))
נשווה בין התוצאה החזויה לתוצאה בפועל.
print('Predicted value: {0:.0f}'.format(y_pred0.reshape(1)[0]))
print('Actual value: {0:.0f}'.format(y[42]))
Predicted value: 224060 Actual value: 156896
יכול להיות שהתוצאות בטווח השגיאה של המודל, אבל מהו טווח השגיאה של המודל? כדי למצוא את טווח השגיאה נעשה את התחזית על כל הדוגמאות.
y_pred = model.predict(x_test)
נחשב את טווח השגיאה:
from sklearn.metrics import mean_absolute_error, mean_squared_error
print('MAE: %.2f' % mean_absolute_error(y_test, y_pred))
print('RMSE: %.2f' % np.sqrt(mean_squared_error(y_test, y_pred)))
MAE: 76116 RMSE: 112223
ככל שהערך של MAE נמוך יותר, כך המודל מדויק יותר.
מדד RMSE הוא היותר שימושי לנו מפני שהוא מבטא את טווח השגיאה באותם היחידות של הטור שאותו אנו מנסים לחזות. לפיכך, טווח השגיאה של המודל הוא +/-112,223 דולר.
סיכום
במדריך הזה למדנו לפתח מודל של רגרסיה שתלוי במספר משתנים בלתי תלויים. למדנו לקודד משתנים קטגוריים ולנרמל את הנתונים הבלתי תלויים למנוע את השפעת שיעור הגודל של עמודות הנתונים השונות. כמו כן, פיתחנו מודל עמוק שכולל שכבה חבויה אחת.
לכל המדריכים בנושא של למידת מכונה
אהבתם? לא אהבתם? דרגו!
0 הצבעות, ממוצע 0 מתוך 5 כוכבים