Проблема

Погода непростая, не так ли? Иногда жарко, иногда холодно. Несмотря на то, что абсолютный прогноз затруднен, приложение для прогнозирования температуры полезно в нашей повседневной жизни.

Цель

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

Кроме того, это приложение пытается построить непрерывный процесс для обеспечения бесперебойного процесса производства и развертывания с помощью CI/CD (подробное объяснение ниже).

Развертывание

Этот пост посвящен стороне развертывания приложения. Подробное объяснение набора данных и модели здесь недоступно. Вместо этого читатели могут проверить этот пост, чтобы глубже погрузиться в этот вопрос.

Бэкэнд

Прежде всего, это приложение инициирует API и проверяет его работоспособность.

# init app
app = FastAPI()

# check status [GET]
@app.get("/")
async def hello():
    return "Hi there, Your API is UP!"

После этого это приложение пытается загрузить модели и проверяет, правильно ли они загружены. В противном случае выдается предупреждение об ошибке.

# check model with api [GET]
@app.get("/check-model")
def check_model():
    # load model
    try:
        model_tn = load_model_tn()
        model_tx = load_model_tx()
        model_tavg = load_model_tavg()
        response = {
            "code": 200,
            "messages": "Model is ready!"
        }
    except Exception as e:
        response = {
            "code": 404,
            "messages": "Model is not ready. Please check your path or model.",
            "error": str(e)
        }
    return response

При входе в процесс прогнозирования это приложение загрузит входные данные, полученные через интерфейс. Прежде чем отправить их в модели, необходимо провести некоторые предварительные работы.

# predict with api [POST]
@app.post("/predict")
async def predict(request: Request):
    # get data from request
    data = await request.json()

    # put all input data into a variable
    predict = [
        data['RH_avg'],
        data['RR'],
        data['ss'],
        data['ff_x'],
        data['ddd_x'],
        data['ff_avg']
    ]

Для числовых столбцов, а именно RH_avg, RR, ss, ff_x, ddd_x и ff_avg, поскольку они имеют форму, аналогичную той, которую требуют модели, дальнейшая работа не требуется, и их можно обработать сразу. Но для ddd_car, month и province_id они являются категориальными переменными, что означает, что должен быть процесс кодирования, позволяющий использовать их моделями.

Поскольку интерфейс предоставляет список этих трех функций, этому приложению необходимо добавить их одну за другой в переменную с именем predict, которая будет использоваться в качестве входных данных для моделей.

    for input in data["ddd_car"]:
        predict.append(input)
    
    predict.append(0) # to input value for unknown ddd_car in model

    for input in data['month']:
        predict.append(input)
    
    for input in data['province_id']:
        predict.append(input)

predict = np.array(predict)
    predict = predict.reshape(1, -1)

    # load model
    model_tn = load_model_tn()
    model_tx = load_model_tx()
    model_tavg = load_model_tavg()

После этого модели прогнозируют и дают соответствующий ответ. Этот ответ, в свою очередь, будет отправлен во фронтенд.

    # predict
    try:
        prediction_tn = round(model_tn.predict(predict)[0])
        prediction_tx = round(model_tx.predict(predict)[0])
        prediction_tavg = round(model_tavg.predict(predict)[0])

        # print(prediction)
        new_line = '\n'
        response = {
            "Code": 200,
            "Message": "Success",
            "Prediction_tavg": f"Average: {prediction_tavg}",
            "Prediction_tn": f"Minimum: {prediction_tn}",
            "Prediction_tx": f"Maximum: {prediction_tx}"
        }
    except Exception as e:
        response = {
            "Code": 404,
            "Message": "Failed",
            "Error": str(e)
        }

    # return response
    return response

Внешний интерфейс

Это приложение использует streamlit для интерфейса. streamlit был выбран из-за простоты внешнего вида и кода.

urllib.request.urlretrieve("https://images.unsplash.com/photo-1584267385494-9fdd9a71ad75?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80", "image.jpeg")
image = Image.open("image.jpeg")
st.image(image)
st.title("Temperature Predictor App")
st.markdown('*easy way to predict temperature*')
st.divider()
st.subheader('Type the value then click Predict button.')

Прежде всего, этот интерфейс открывается с формами для входных данных, которые будут использоваться моделями прогнозирования в серверной части. Поскольку имеется три категориальные переменные и, кроме того, три со многими метками, код может выглядеть немного запутанным, поскольку позволяет вводить пользовательский ввод в форме, аналогичной предпосылке модели.

with st.form("iris-app-form"):
    ddd_car = st.selectbox('Arah Angin Terkencang:',
    ('C',
    'N',
    'E',
    'S',
    'W',
    'NE',
    'SE',
    'SW',
    'NW'))

    province_id = st.selectbox('Provinsi Domisili:',
    ('Nanggroe Aceh Darussalam',
    'Sumatera Utara',
    'Sumatera Barat',
    'Riau',
    'Jambi',
    'Sumatera Selatan',
    'Bengkulu',
    'Lampung',
    'Kep. Bangka Belitung',
    'Kep. Riau',
    'DKI Jakarta',
    'Jawa Barat',
    'Jawa Tengah',
    'DI Yogyakarta',
    'Jawa Timur',
    'Banten',
    'Bali',
    'Nusa Tenggara Barat',
    'Nusa Tenggara Timur',
    'Kalimantan Barat',
    'Kalimantan Tengah',
    'Kalimantan Selatan',
    'Kalimantan Timur',
    'Sulawesi Utara',
    'Sulawesi Tengah',
    'Sulawesi Selatan',
    'Sulawesi Tenggara',
    'Gorontalo',
    'Sulawesi Barat',
    'Maluku',
    'Maluku Utara',
    'Papua',
    'Papua Barat',
    'Kalimantan Utara'))

    month = st.selectbox('Bulan:', (
        'Januari',
        'Februari',
        'Maret',
        'April',
        'Mei',
        'Juni',
        'Juli',
        'Agustus',
        'September',
        'Oktober',
        'November',
        'Desember'
    ))

    RH_avg = st.number_input("Rerata Kelembaban (%)", help="Nilai antara 0 - 100")
    RR = st.number_input("Curah Hujan (mm)", help="Nilai harus > 0.")
    ss = st.number_input("Durasi Sinar Matahari (hour)", help="Nilai harus > 0.")
    ff_x = st.number_input("Kecepatan Angin Maksimal (m/s)", help="Nilai harus > 0.")
    ddd_x = st.number_input("Arah Angin Saat Kecepatan Maksimal (°)", help="Nilai antara 1 - 360.")
    ff_avg = st.number_input("Kecepatan Angin Rata-rata (m/s)", help="Nilai harus > 0.")

    # submit button
    submitted = st.form_submit_button("Predict")

Следует отметить, что эти три категориальные переменные должны пройти процесс кодирования. Здесь эта модель использует One-Hot Encoding (OHE), поскольку между метками нет иерархии. Вследствие OHE в этой модели используется множество столбцов.

Вот почему необходимо проделать работу по обработке категориальных переменных. Из-за длины кода он не будет показан здесь полностью (доступен на GitHub). Дело в том, что необходим процесс, который способен преобразовывать вводимые пользователем данные в соответствующие формы в соответствии с моделью.

Местный

Запустите бэкэнд с помощью:

uvicorn temperature-backend:app --reload

Запустите интерфейс с помощью

streamlit run temperature-frontend.py

GitHub Push

Убедившись, что приложение отлично работает на локальном компьютере, мы помещаем его на GitHub.

АВС

Как только приложение окажется в репозитории GitHub, мы используем AWS для его запуска.

Сначала мы клонируем репо, используя:

git clone <repo link>

А затем создайте и запустите образ докера, используя:

sudo docker compose up -d --build

Как только приложение будет запущено и заработает, мы сможем его попробовать. Это приложение доступно по адресу http://52.221.187.13:8501/.

CI/CD

После загрузки приложения в AWS и проверки его работоспособности наша работа фактически завершена. Но мы знаем, что приложение — это не разовая работа, в будущем есть еще (даже гораздо больше) версий и обновлений. Делать их вручную, конечно, возможно. Но это также означает, что нам придется выполнять каждые шаги: от загрузки новых кодов в GitHub до создания новых образов Docker.

Чтобы лучше адаптироваться к этим изменениям, мы применяем непрерывную интеграцию/непрерывное развертывание(CI/CD), чтобы обеспечить бесперебойное обновление приложения. После завершения CI/CD доработка/обновление осуществляется всего одним щелчком мыши.

CI/CD можно выполнить на GitHub, используя действия GitHub и секретные переменные. Полный код действий доступен на GitHub. Что касается секретных переменных, они уникальны для каждого. Короче говоря, их можно получить:

ПОЛЬЗОВАТЕЛЬ: имя пользователя, используемое для подключения к экземпляру AWS.

KEY: значение пары ключей от AWS.

ХОСТ: хост из экземпляра AWS.

По сути, CI/CD пытается перезапустить весь процесс каждый раз, когда что-то срабатывает. В этом случае каждый раз происходит загрузка в репозиторий.

Заключение и понимание

  1. Существует разница между локальным и серверным.
  2. Ранее это приложение использовало метод match, который был недоступен на сервере, хотя использовался Python 3.10.5. Поскольку проблема осталась нерешенной, вместо нее используется ручная функция def.
  3. Репо доступен здесь и приложение здесь.