В этом руководстве мы используем лучшую модель для прогнозирования цен перепродажи HDB и предоставляем ее в качестве конечной точки на Flask. У нас будет конечная точка обучения и конечная точка прогнозирования отдельно. Чтобы проверить точность предсказания на себе, вы можете зайти на hdbpricer.com

Это вторая часть руководства из четырех частей
1. Создание хорошей модели прогнозирования
2. Размещение прогнозирования модели в качестве конечной точки API на Flask
3. Создание простого интерфейса VueJS, чтобы пользователи могли оценивать свои HDB
4. Развертывание всего приложения полного стека в Интернете

Git-репозиторий для реализации можно найти здесь
Мой сервер приложений hdbpricer

Задний план

В первой части руководства мы обнаружили, что K ближайших соседей дают наилучший результат прогнозирования. Следовательно, мы упаковали его для использования внешним приложением (часть 3 руководства из 4 частей).

Настройка сервера (app.py)

Во-первых,

  • Импортировать необходимые библиотеки
  • Настроить CORS
from flask import Flask, jsonify, request 
from flask_cors import CORS 
import random 
from predict import predictPrice 
from train import train 
import os # configuration 
DEBUG = True 
# instantiate the app app = Flask(__name__) app.config.from_object(__name__) 
# enable CORS CORS(app, resources={r'/*': {'origins': '*'}})

Вот пример того, что хранит приложение flask. Список HDB dict с атрибутами цена перепродажи

HDBs = [ 
{ 'town': 'ANG MO KIO', 'flat_type': '2 ROOM', 'storey_range': '10 TO 12', 'floor_area_sqm': 44.0, 'lease_commence_date': 1979, 'resale_price': 232000.0, }, 
#town, flat_type,storey_range,floor_area_sqm,lease_commence_date 
]

Затем мы настраиваем маршруты.

ping: чтобы проверить, работает ли сервер нормально

# sanity check route 
@app.route('/ping', methods=['GET']) 
def ping_pong(): 
   return jsonify('pong!')

hdbs:
— метод GET возвращает все hdb из списка
— метод POST получает полезную нагрузку информации HDB, запускает функцию прогнозирования, а затем добавляет новый hdb в список

@app.route('/hdbs', methods=['GET', 'POST'])
def all_hdbs():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
HDBs.append({
'town': post_data.get('town'),
'flat_type': post_data.get('flat_type'),
'storey_range': post_data.get('storey_range'),
'floor_area_sqm': post_data.get('floor_area_sqm'),
'lease_commence_date': post_data.get('lease_commence_date'),
'resale_price': round(predictPrice( town = post_data.get('town'),flat_type=post_data.get('flat_type'),storey_range=post_data.get('storey_range'),floor_area_sqm=post_data.get('floor_area_sqm'),lease_commence_date=post_data.get('lease_commence_date'))*1.01), # To return from model
})
response_object['message'] = 'Priced!'
else:
response_object['hdbs'] = HDBs
return jsonify(response_object)

train: запускает метод обучения и возвращает результат последнего обучения. Это нужно будет использовать только тогда, когда у нас будут новые наборы данных для обучения.

@app.route('/train', methods=['GET'])
def train_model():
response_object = {'status': 'success'}
response_object['score'] = train()
return jsonify(response_object)

И последнее, но не менее важное: нам понадобится это, чтобы иметь возможность запускать python app.py как локально, так и на сервере. (Развертывание на сервере будет описано в части 4 руководства, состоящего из 4 частей)

По сути, если для «ПОРТ» есть переменная среды, это означает, что приложение развертывается на сервере и будет использовать соответствующий номер порта. В противном случае он будет размещаться на порту 5000 на вашем локальном хосте.

if __name__ == '__main__':
port = int(os.getenv('PORT', 5000))
print("Starting app on port %d" % port)
if(port!=5000):
app.run(debug=False, port=port, host='0.0.0.0')
else:
app.run()

Вот полный блок кода для app.py

from flask import Flask, jsonify, request
from flask_cors import CORS
import random
from predict import predictPrice
from train import train
import os
# configuration
DEBUG = True
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
HDBs = [
{
'town': 'ANG MO KIO',
'flat_type': '2 ROOM',
'storey_range': '10 TO 12',
'floor_area_sqm': 44.0,
'lease_commence_date': 1979,
'resale_price': 232000.0,
},
#town, flat_type,storey_range,floor_area_sqm,lease_commence_date
]
# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')
@app.route('/hdbs', methods=['GET', 'POST'])
def all_hdbs():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
HDBs.append({
'town': post_data.get('town'),
'flat_type': post_data.get('flat_type'),
'storey_range': post_data.get('storey_range'),
'floor_area_sqm': post_data.get('floor_area_sqm'),
'lease_commence_date': post_data.get('lease_commence_date'),
'resale_price': round(predictPrice( town = post_data.get('town'),flat_type=post_data.get('flat_type'),storey_range=post_data.get('storey_range'),floor_area_sqm=post_data.get('floor_area_sqm'),lease_commence_date=post_data.get('lease_commence_date'))*1.01), # To return from model
})
response_object['message'] = 'Priced!'
else:
response_object['hdbs'] = HDBs
return jsonify(response_object)
@app.route('/train', methods=['GET'])
def train_model():
response_object = {'status': 'success'}
response_object['score'] = train()
return jsonify(response_object)
if __name__ == '__main__':
port = int(os.getenv('PORT', 5000))
print("Starting app on port %d" % port)
if(port!=5000):
app.run(debug=False, port=port, host='0.0.0.0')
else:
app.run()

Теперь перейдем к API обучения.

поезд.ру

Импортировать необходимые библиотеки

import math
from collections import defaultdict
import numpy as np
from numpy import unique
import pandas as pd
from sklearn.preprocessing import StandardScaler, LabelEncoder
import geopy
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
from sklearn.neighbors import KNeighborsRegressor
import pickle

Вы увидите, что код очень похож на блокнот Jupyter.

Краткий обзор, вот что мы делаем

  1. Чтение набора данных
  2. Предварительно обработать данные
  3. Геокодировать города
  4. Кодировать строковые данные в целые числа
  5. Удалить столбцы
  6. Разделите данные на обучение и тестирование
  7. Масштабирование
  8. Тренируйтесь и тренируйтесь

Выполнена дополнительная работа:
1. Сохранение масштабатора
2. Сохранение модели
3. Оценка сохраненной модели

Это сделано для того, чтобы модель и средство масштабирования могли быть повторно использованы функцией прогнозирования ( predict.py) позже. Здесь мы используем Pickle, но и joblib работает нормально.

def train():
#Dataset from https://data.gov.sg/dataset/resale-flat-prices
file_url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vQ8OfO82KXoRmO0E6c58MdwsOSc8ns5Geme87SiaiqTUrS_hI8u8mYE5KIOfQe4m2m3GGf9En22xuXx/pub?gid=382289391&single=true&output=csv"
data = pd.read_csv(file_url)
dataframe = data.copy()
#let's break date to years, months
dataframe['date'] = pd.to_datetime(dataframe['month'])
dataframe['month'] = dataframe['date'].apply(lambda date:date.month)
dataframe['year'] = dataframe['date'].apply(lambda date:date.year)
#Get number of years left on lease as a continuous number (ignoring months)
dataframe['remaining_lease'] = dataframe['remaining_lease'].apply(lambda remaining_lease:remaining_lease[:2])
#Get storey range as a continuous number
dataframe['storey_range'] = dataframe['storey_range'].apply(lambda storey_range:storey_range[:2])
#Concat address
dataframe['address'] = dataframe['block'].map(str) + ', ' + dataframe['street_name'].map(str) + ', Singapore'
'''
#Geocode by address
locator = Nominatim(user_agent="myGeocoder")
# 1 - convenient function to delay between geocoding calls
geocode = RateLimiter(locator.geocode, min_delay_seconds=1)
# 2- - create location column
dataframe['location'] = dataframe['address'].apply(geocode)
print("step 2")
# 3 - create longitude, laatitude and altitude from location column (returns tuple)
dataframe['point'] = dataframe['location'].apply(lambda loc: tuple(loc.point) if loc else None)
print("step 3")
# 4 - split point column into latitude, longitude and altitude columns
dataframe[['latitude', 'longitude', 'altitude']] = pd.DataFrame(dataframe['point'].tolist(), index=df.index)
print("step 4")
'''
#Geocode by town (Singapore is so small that geocoding by addresses might not make much difference compared to geocoding to town)
town = [x for x in dataframe['town'].unique().tolist()
if type(x) == str]
latitude = []
longitude =  []
for i in range(0, len(town)):
# remove things that does not seem usefull here
try:
geolocator = Nominatim(user_agent="ny_explorer")
loc = geolocator.geocode(town[i])
latitude.append(loc.latitude)
longitude.append(loc.longitude)
#print('The geographical coordinate of location are {}, {}.'.format(loc.latitude, loc.longitude))
except:
# in the case the geolocator does not work, then add nan element to list
# to keep the right size
latitude.append(np.nan)
longitude.append(np.nan)
# create a dataframe with the locatio, latitude and longitude
df_ = pd.DataFrame({'town':town,
'latitude': latitude,
'longitude':longitude})
# merge on Restaurant_Location with rest_df to get the column
dataframe = dataframe.merge(df_, on='town', how='left')
### label encode the categorical values and convert them to numbers
'''
le = LabelEncoder()
dataframe['town']= le.fit_transform(dataframe['town'].astype(str))
dataframe['flat_type'] = le.fit_transform(dataframe['flat_type'].astype(str))
dataframe['street_name'] = le.fit_transform(dataframe['street_name'].astype(str))
#dataframe['storey_range'] = le.fit_transform(dataframe['storey_range'].astype(str))
dataframe['flat_model'] = le.fit_transform(dataframe['flat_model'].astype(str))
dataframe['block'] = le.fit_transform(dataframe['block'].astype(str))
dataframe['address'] = le.fit_transform(dataframe['address'].astype(str))
'''
townDict = {'ANG MO KIO': 1,'BEDOK': 2,'BISHAN': 3,'BUKIT BATOK': 4,'BUKIT MERAH': 5,'BUKIT PANJANG': 6,'BUKIT TIMAH': 7,'CENTRAL AREA': 8,'CHOA CHU KANG': 9,'CLEMENTI': 10,'GEYLANG': 11,'HOUGANG': 12,'JURONG EAST': 13,'JURONG WEST': 14,'KALLANG/WHAMPOA': 15,'MARINE PARADE': 16,'PASIR RIS': 17,'PUNGGOL': 18,'QUEENSTOWN': 19,'SEMBAWANG': 20,'SENGKANG': 21,'SERANGOON': 22,'TAMPINES': 23,'TOA PAYOH': 24,'WOODLANDS': 25,'YISHUN': 26,}
flat_typeDict = {'1 ROOM': 1,'2 ROOM': 2,'3 ROOM': 3,'4 ROOM': 4,'5 ROOM': 5,'EXECUTIVE': 6,'MULTI-GENERATION': 7,}
dataframe['town'] = dataframe['town'].replace(townDict, regex=True)
dataframe['flat_type'] = dataframe['flat_type'].replace(flat_typeDict, regex=True)
# drop some unnecessary columns
dataframe = dataframe.drop('date',axis=1)
dataframe = dataframe.drop('block',axis=1)
#dataframe = dataframe.drop('lease_commence_date',axis=1)
dataframe = dataframe.drop('month',axis=1)
dataframe = dataframe.drop('street_name',axis=1)
dataframe = dataframe.drop('address',axis=1)
dataframe = dataframe.drop('flat_model',axis=1)
#dataframe = dataframe.drop('town',axis=1)
dataframe = dataframe.drop('year',axis=1)
#dataframe = dataframe.drop('latitude',axis=1)
dataframe = dataframe.drop('remaining_lease',axis=1)
X = dataframe.drop('resale_price',axis =1)
y = dataframe['resale_price']
X=X.values
y=y.values
#splitting Train and Test
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=101)
#standardization scaler - fit&transform on train, fit only on test
s_scaler = StandardScaler()
X_train = s_scaler.fit_transform(X_train.astype(np.float))
X_test = s_scaler.transform(X_test.astype(np.float))
knn = KNeighborsRegressor(algorithm='brute')
knn.fit(X_train,y_train)
#save model
filename = 'hdbknn.sav'
scalername = 'scaler.sav'
pickle.dump(knn, open(filename, 'wb'))
pickle.dump(s_scaler, open(scalername, 'wb'))
loaded_model = pickle.load(open(filename, 'rb'))
result = loaded_model.score(X_test, y_test)
print(result)
return result

Без дальнейших церемоний, давайте посмотрим на функцию прогнозирования.

предсказать.py

Импортировать необходимые библиотеки

import math
from collections import defaultdict
import numpy as np
from numpy import unique
import pandas as pd
from sklearn.preprocessing import StandardScaler
import geopy
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
from sklearn.neighbors import KNeighborsRegressor
import pickle

Определите функцию для получения входных данных от REST API

def predictPrice(town,flat_type,storey_range,floor_area_sqm,lease_commence_date):
#town, flat_type,storey_range,floor_area_sqm,lease_commence_date
input_data = {
'town': town,
'flat_type': flat_type,
'storey_range': storey_range,
'floor_area_sqm': floor_area_sqm,
'lease_commence_date': lease_commence_date,
}

Геокодировать город (будущие версии hdbpricer также смогут геокодировать точные адреса, поскольку это буквально тот же метод)

#Geocode by town (Singapore is so small that geocoding by addresses might not make much difference compared to geocoding to town)
town = input_data["town"]
latitude = 0
longitude =  0
try:
geolocator = Nominatim(user_agent="ny_explorer")
loc = geolocator.geocode(town)
latitude= loc.latitude
longitude = loc.longitude
#print('The geographical coordinate of location are {}, {}.'.format(loc.latitude, loc.longitude))
except:
# in the case the geolocator does not work, then add nan element
# to keep the right size
latitude = np.nan
longitude = np.nan
input_data['latitude'] = latitude
input_data['longitude'] = longitude
input_data['storey_range'] = input_data['storey_range'][:2]

Кодировать строковые значения

townDict = {'ANG MO KIO': 1,'BEDOK': 2,'BISHAN': 3,'BUKIT BATOK': 4,'BUKIT MERAH': 5,'BUKIT PANJANG': 6,'BUKIT TIMAH': 7,'CENTRAL AREA': 8,'CHOA CHU KANG': 9,'CLEMENTI': 10,'GEYLANG': 11,'HOUGANG': 12,'JURONG EAST': 13,'JURONG WEST': 14,'KALLANG/WHAMPOA': 15,'MARINE PARADE': 16,'PASIR RIS': 17,'PUNGGOL': 18,'QUEENSTOWN': 19,'SEMBAWANG': 20,'SENGKANG': 21,'SERANGOON': 22,'TAMPINES': 23,'TOA PAYOH': 24,'WOODLANDS': 25,'YISHUN': 26,}
flat_typeDict = {'1 ROOM': 1,'2 ROOM': 2,'3 ROOM': 3,'4 ROOM': 4,'5 ROOM': 5,'EXECUTIVE': 6,'MULTI-GENERATION': 7,}
input_data['town'] = townDict[input_data['town']]
input_data['flat_type'] = flat_typeDict[input_data['flat_type']]

Преобразовать в фрейм данных

dataframe = pd.DataFrame.from_records([input_data]) 
data = dataframe.values

Масштабирование с помощью нашего сохраненного скейлера. Это важно для использования сохраненного скейлера, потому что вы не должны подгонять свой скейлер на основе новых/тестовых данных. Скалер был подобран с использованием обучающих данных в train.py.

scalername = 'scaler.sav' 
s_scaler = pickle.load(open(scalername, 'rb')) 
data = s_scaler.transform(data.astype(np.float))

Предсказывать!

filename = 'hdbknn.sav' 
loaded_model = pickle.load(open(filename, 'rb')) 
result = loaded_model.predict(data) 
#print(result) 
return result[0]

Полный файл predict.py

import math
#import tensorflow as tf
from collections import defaultdict
import numpy as np
from numpy import unique
import pandas as pd
from sklearn.preprocessing import StandardScaler
#from tensorflow import keras
#from tensorflow.keras import layers
#import geopandas as gpd
import geopy
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
from sklearn.neighbors import KNeighborsRegressor
import pickle
def predictPrice(town,flat_type,storey_range,floor_area_sqm,lease_commence_date):
#town, flat_type,storey_range,floor_area_sqm,lease_commence_date
input_data = {
'town': town,
'flat_type': flat_type,
'storey_range': storey_range,
'floor_area_sqm': floor_area_sqm,
'lease_commence_date': lease_commence_date,
}
#Geocode by town (Singapore is so small that geocoding by addresses might not make much difference compared to geocoding to town)
town = input_data["town"]
latitude = 0
longitude =  0
try:
geolocator = Nominatim(user_agent="ny_explorer")
loc = geolocator.geocode(town)
latitude= loc.latitude
longitude = loc.longitude
#print('The geographical coordinate of location are {}, {}.'.format(loc.latitude, loc.longitude))
except:
# in the case the geolocator does not work, then add nan element
# to keep the right size
latitude = np.nan
longitude = np.nan
input_data['latitude'] = latitude
input_data['longitude'] = longitude
input_data['storey_range'] = input_data['storey_range'][:2]
townDict = {'ANG MO KIO': 1,'BEDOK': 2,'BISHAN': 3,'BUKIT BATOK': 4,'BUKIT MERAH': 5,'BUKIT PANJANG': 6,'BUKIT TIMAH': 7,'CENTRAL AREA': 8,'CHOA CHU KANG': 9,'CLEMENTI': 10,'GEYLANG': 11,'HOUGANG': 12,'JURONG EAST': 13,'JURONG WEST': 14,'KALLANG/WHAMPOA': 15,'MARINE PARADE': 16,'PASIR RIS': 17,'PUNGGOL': 18,'QUEENSTOWN': 19,'SEMBAWANG': 20,'SENGKANG': 21,'SERANGOON': 22,'TAMPINES': 23,'TOA PAYOH': 24,'WOODLANDS': 25,'YISHUN': 26,}
flat_typeDict = {'1 ROOM': 1,'2 ROOM': 2,'3 ROOM': 3,'4 ROOM': 4,'5 ROOM': 5,'EXECUTIVE': 6,'MULTI-GENERATION': 7,}
input_data['town'] = townDict[input_data['town']]
input_data['flat_type'] = flat_typeDict[input_data['flat_type']]
dataframe = pd.DataFrame.from_records([input_data])
data = dataframe.values
scalername = 'scaler.sav'
s_scaler = pickle.load(open(scalername, 'rb'))
data = s_scaler.transform(data.astype(np.float))
#print(data)
filename = 'hdbknn.sav'
loaded_model = pickle.load(open(filename, 'rb'))
result = loaded_model.predict(data)
#print(result)
return result[0]

Запуск сервера

В зависимости от того, используете ли вы Conda или просто Vanilla Python

Установите свои зависимости (pip или conda работают нормально)

pip install -r requirements.txt

Запустить сервер

  1. Питон
env\Scripts\activate
(env) python server/app.py
  1. Конда
conda activate modelenv
python server/app.py

Вывод

Это все о том, как разместить ваши модели машинного обучения, модели обучения/прогнозирования через REST API с использованием библиотеки Python Flask. Некоторое вдохновение было взято отсюда. В нашем следующем уроке вы можете узнать о создании клиентского приложения с использованием VueJS!

Дополнительное примечание: это не готовая к работе реализация, не рекомендуется делать флягу «python app.py» для реальных случаев использования в производственной среде 🙂

Первоначально опубликовано на http://royleekiat.com 30 октября 2020 г.