Вот история о том, как однажды я решил создать какой-нибудь причудливый интернет-сервис. Я подумал, машинное обучение — это своего рода тренд, не так ли? Посмотрим, что я могу сделать…
Существует множество интернет-сервисов на основе машинного обучения, таких как sonix.ai и, очевидно, ChatGPT, а также другие, такие как synthesia.io, deepl.com/translator, photoai.com и многие другие. Люди заходят на эти сайты, чтобы извлечь текст из аудио, чрезвычайно интеллектуальный текст из подсказок, видео, созданное из текста, текст на другом языке, созданный из текста, и изображения, созданные из изображений.
Итак, как я могу сделать такой сайт? — подумал я. Сайт, который создает изображение с граничными рамками из изображения, может быть хорошим началом. Итак, вот я: создал детектор объектов. [Кстати, ссылка, вероятно, останется рабочей навсегда, но базовый детектор объектов находится на несвободном экземпляре AWS, поэтому я, вероятно, в какой-то момент отключу его.]
Итак, как же это работает, спросите вы? Просто зайдите сюда: https://object-detector-2000.netlify.app/
Затем загрузите образ из Интернета на свой жесткий диск или просто выберите существующий образ на жестком диске. Затем нажмите Выбрать файл и укажите локальный путь к этому изображению.
Затем нажмите Открыть и немного подождите, пока детектор объектов сделает свое волшебство, и вы просто сможете увидеть что-то вроде этого:
Вот так, уже. Не стесняйтесь загрузить еще пару изображений и посмотреть, какие объекты обнаружит сайт и как он их назовет!
Давайте рассмотрим еще один пример:
Почему не все машины распознаются?
Это здорово, но как я это сделал?
История начинается на этой веб-странице: https://ultralytics.com/
На этой веб-странице представлена модель YOLOv8. Вы можете получить код Python оттуда, и он будет запускать модель YOLOv8 на любом изображении, которое вы загружаете в свой Python. Я подумал, что это круто, но я хотел иметь веб-сервис. Итак, я вспомнил о пакете Python flask. Затем я варил, варил и варил, пока не получилось вот это:
import flask import glob import os import ssl import ultralytics app = flask.Flask(__name__) @app.route('/yolo', methods=['GET', 'POST']) def get_traces_handle(): try: with open('received_image.jpg', 'wb') as binary_image: binary_image.write(flask.request.data) model = ultralytics.YOLO("yolov8n.pt") results = model.predict( source='received_image.jpg', save=True, ) # Get all files in the current directory files = glob.glob('./runs/**/*', recursive=True) # Find the most recently modified file latest_file = max(files, key=os.path.getmtime) # Open the file in binary mode and read it with open(latest_file, 'rb') as f: file_bytes = f.read() # Create a response with the file bytes flask_response = flask.make_response(file_bytes) # Set the appropriate content type flask_response.headers.set('Content-Type', 'image/jpeg') # Optionally, set the content disposition to allow for file download flask_response.headers.set('Content-Disposition', 'attachment', filename='filename.jpg') return flask_response except Exception as exception: error_text = str(exception) flask_response = flask.make_response(error_text) flask_response.headers['Content-Type'] = 'text/plain; charset=utf-8' return flask_response @app.after_request def add_cors_headers(response): response.headers.add('Access-Control-Allow-Origin', '*') response.headers.add('Access-Control-Allow-Headers', 'Content-Type') return response if __name__ == '__main__': context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain('certificate.pem', 'key.pem') app.run( host='0.0.0.0', port=8080, debug=True, use_reloader=False, ssl_context=context, )
На самом деле не такая уж и сложность. Весь код на самом деле делает три вещи:
- Настройте веб-сервер для получения изображений от таких клиентов, как приложения ReactJS, клиенты Postman или даже другие программы Python.
- В веб-сервисе получите изображение в виде строки байтов, запишите его на жесткий диск и укажите путь к изображению для модели YOLOv8. Затем подождите, пока модель сделает свое дело, и прочитайте последний измененный файл в каталоге runs/, специфичном для YOLOv8. Этот файл будет нашим выходным изображением с граничными рамками. Отправьте файл запрашивающему.
- На самом деле мы используем SSL-сертификаты, поэтому веб-сервер будет иметь определенную подпись в виде пары файлов: SSL-сертификат + SSL-ключ. Из-за этого мы можем обращаться к нашему серверу с URL-адресом, начинающимся с https://, что будет важно, поскольку мы также будем размещать наш пользовательский интерфейс на https://, и вы не можете адресовать URL-адрес http:// из https:// :// URL (мне пришлось выучить его на собственном горьком опыте во время этого проекта).
Назовем этот файл Python api.py.
Итак, давайте запустим api.py локально! Ах, подождите, сначала requirements.txt:
flask jupyter ultralytics watchdog
И вот небольшой сценарий оболочки для установки этих требований и запуска приведенного выше кода Python:
# Delete the priorly created Anaconda3 environment conda activate base rm -rf $(conda info --base)/envs/object_detection/ conda env remove --name object_detection # Create a new Anaconda3 environment conda create --yes --name object_detection python conda activate object_detection pip install --upgrade --requirement requirements.txt # Run the actual Python file python api.py
На данный момент мы будем работать без поддержки HTTPS, поэтому перепишите вызов как
if __name__ == '__main__': # context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # context.load_cert_chain('certificate.pem', 'key.pem') app.run( host='0.0.0.0', port=8080, debug=True, use_reloader=False, # ssl_context=context, )
в приведенном выше файле Python.
Теперь зайдите в Почтальон и установите программу на свой локальный компьютер. Мы хотим отправить изображение в этот API и посмотреть, вернется ли что-нибудь.
Если бы вы были внимательны, то заметили бы, что мы обслуживаем HTTP-запросы по пути /yolo. Итак, мы должны иметь возможность обратиться к этому URL: http://127.0.0.1:8080/yolo. Давайте откроем Postman и подготовим HTTP-запрос следующим образом:
Если вы отправите его, вы получите:
Изображение не найдено […]/received_image.jpg
Этот файл received_image.jpg — это файл, который сервер Python создает при получении запроса HTTP/HTTPS. Но на этот раз мы не отправляли изображения на сервер, поэтому получаем эту ошибку.
Давайте настроим изображение в HTTP-POST-Body с помощью Postman:
Совет: вам нужно прокрутить раскрывающееся меню, чтобы найти «двоичный» тип тела. Затем просто выберите локальный путь к вашему изображению.
Хорошо: это сработало хорошо. Теперь давайте фактически настроим HTTPS: для этого нам нужно использовать программное обеспечение под названием mkcert.
Сначала установите программное обеспечение, а затем установите в своей системе локальный центр сертификации SSL. Я не точно понимаю, как это работает, но полагаю, что программа mkcert связывается с вашим браузером Chromium или Firefox, сообщая им, что определенный SSL-сертификат, если он представлен через HTTPS-сервер, безопасен. Это взаимодействие происходит в то самое время, когда mkcert создает этот SSL-сертификат и соответствующий ему ключ. Затем мы перенесем сертификат и ключ на сервер HTTPS, чтобы сервер мог использовать эти вещи для аутентификации в отношении любопытных веб-браузеров.
Хватит говорить! Вот так:
# Download the mkcert binary curl --insecure -JLO "https://dl.filippo.io/mkcert/v1.4.4?for=linux/amd64" chmod +x mkcert-v1.4.4-linux-amd64 # Make up a pair of SSL certificate + key, as detailed above. ./mkcert-v1.4.4-linux-amd64 -install ./mkcert-v1.4.4-linux-amd64 localhost # Rename the files for convenience mv localhost.pem certificate.pem mv localhost-key.pem key.pem
Теперь верните эти строки в файл Python:
if __name__ == '__main__': context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain('certificate.pem', 'key.pem') app.run( host='0.0.0.0', port=8080, debug=True, use_reloader=False, ssl_context=context, )
Бегать:
python api.py
Мы проверим службу HTTPS, введя URL-адрес: https://127.0.0.1:8080/yolo.
Но теперь мы получаем эту ошибку:
Ошибка SSL: невозможно проверить первый сертификат
Это означает, что Postman не знает SSL-сертификат mkcert. На самом деле это должно быть решаемо, но я не понял. Поэтому просто нажмите Отключить проверку SSL:
Красивый!
Теперь у нас есть HTTPS-сервер, работающий в нашей локальной системе, который рисует ограничивающие рамки в изображениях, которые мы ему передаем. Очень круто, привет!
А как же Терраформ?
Это действительно здорово, но никто не может видеть наш сервис, кроме нас, нашего кота и, возможно, некоторых соседей, которые используют наш WiFi против нашей воли. Как мы можем сознательно выйти на всеобщее обозрение?
Конечно же, обратимся к Amazon и его платформе AWS.
Когда вы находитесь на платформе AWS, выполните следующие действия:
- Перейдите в раздел Управление идентификацией и доступом (IAM).
- Нажмите Управление доступом > Пользователи. Нажмите Добавить пользователей.
- Введите имя пользователя, затем нажмите Прикрепить политики напрямую > Создать политику.
- Переключитесь с Visual на JSON.
- Затем вставьте этот код в поле:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "ec2:*", "Resource": "*" } ] }
Мы позволим вашей учетной записи пользователя AWS создавать, изменять и уничтожать произвольные вычислительные ресурсы и экземпляры AWS.
6. Нажмите Далее.
7. Введите имя для этой политики, например EC2FullAccessObjectDetector. Нажмите Создать политику.
8. Перейдите на вкладку с кнопкой Создать политику.
9. Обновите политики с помощью кнопки с кружком слева. Теперь найдите политику EC2FullAccessObjectDetector. Выберите его с помощью флажка слева. Затем нажмите Далее и создайте пользователя.
10. Перейдите на страницу пользователя. Перейдите в раздел Учетные данные безопасности > Ключи доступа.
11. Нажмите Создать ключ доступа. Выберите Сторонний сервис. Установите флажок и перейдите к Далее.
12. Загрузить CSV-файл.
13. Откройте файл .csv и найдите там две важные части информации: идентификатор вашего ключа доступа (AWS_ACCESS_KEY_ID) и ваш секретный ключ доступа (AWS_SECRET_ACCESS_KEY). сильный>).
Эти ключи принадлежат созданному пользователю платформы AWS. Платформу AWS можно настроить с помощью Terraform, который представляет собой не что иное, как формальный язык для настройки платформы AWS. Возможно, у вас был опыт посещения AWS, и вы часами щелкали, щелкали и щелкали, только чтобы обнаружить, что небольшая ошибка, которую вы сделали при нажатии, заставляет вас щелкать еще больше. Вместо этого, используя Terraform, вы просто пишете, какие экземпляры AWS вам нужны с точки зрения объема памяти или хранилища, какого типа ЦП и какой открытый ключ SSH они должны иметь, а Terraform переходит к AWS и щелкает там. от вашего имени.
Давайте проверим это. Вот немного холодной воды для вас, чтобы прыгнуть:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.16" } } required_version = ">= 1.2.0" } provider "aws" { region = "us-west-2" } resource "aws_security_group" "app_sg" { name = "app_sg" description = "Allow SSH and TCP traffic on port 8080" ingress { description = "SSH" from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { description = "TCP" from_port = 8080 to_port = 8080 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_instance" "app_server" { ami = "ami-08d70e59c07c61a3a" instance_type = "t2.small" associate_public_ip_address = true key_name = "id_ed25519" vpc_security_group_ids = [aws_security_group.app_sg.id] tags = { Name = "ObjectDetector" } root_block_device { volume_size = 32 } } resource "aws_key_pair" "ssh-key" { key_name = "id_ed25519" public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVlCPR6Uvbz5XxdgfQTat2jZc3gM9mi9FSj7sRDagFQ dimitri@tokyo" }
В дополнение к тому, что описано выше, мы также открываем два порта на нашем экземпляре: 22 (который принадлежит SSH) и 8080 (который будет портом нашего детектора объектов). Конечно, нам нужен SSH, чтобы войти в экземпляр AWS и запустить настоящий веб-сервис. Для этого вы должны заменить свойство public_key в ssh-key своим открытым ключом, соответствующий закрытый ключ которого у вас есть.
Назовем указанный выше файл main.tf. Также у нас есть еще один файл outputs.tf:
output "public_ip" { description = "The public IP for ssh access" value = aws_instance.app_server.public_ip }
Теперь давайте сделаем что-нибудь с этой конфигурацией Terraform. Давайте снова откроем нашу оболочку bash:
export AWS_ACCESS_KEY_ID=1234567890 export AWS_SECRET_ACCESS_KEY=1234567890 env | grep AWS
Это установит наши переменные среды. Конечно, вставьте свои реальные значения, которые вам нужно будет получить из AWS, вручную щелкая по платформе.
Далее мы заставим Terraform выполнять клики:
terraform init terraform apply
Вам нужно будет ввести да где-нибудь во время этого процесса.
Так или иначе, на этот раз мы запоминаем IP-адрес инстанса AWS, созданного для нас Terraform:
export DEPLOYED_IP=$(terraform output -raw public_ip)
Теперь просто создайте mkcert SSL-сертификат + ключ, скопируйте их в экземпляр AWS и перейдите туда самостоятельно:
rm -f *.pem mkcert* curl --insecure -JLO "https://dl.filippo.io/mkcert/v1.4.4?for=linux/amd64" chmod u+x mkcert-v1.4.4-linux-amd64 ./mkcert-v1.4.4-linux-amd64 -install ./mkcert-v1.4.4-linux-amd64 $DEPLOYED_IP scp $DEPLOYED_IP.pem ubuntu@$DEPLOYED_IP:/home/ubuntu/certificate.pem scp $DEPLOYED_IP-key.pem ubuntu@$DEPLOYED_IP:/home/ubuntu/key.pem ssh ubuntu@$DEPLOYED_IP
Предупреждение. Это последний момент, когда вы заметите, что не предоставили Terraform свой собственный открытый ключ. Если у вас возникла ошибка аутентификации SSH, вернитесь к настройке Terraform и настройте ssh-key с помощью собственного ключа SSH.
Что теперь? Конечно, нам нужны эти файлы Python api.py и requirements.txt прямо сейчас. Их вы также можете скопировать с помощью scp, но я предпочитаю использовать GitHub.
Просто поместите эти два файла в отдельный каталог (например, object_detection/) и запустите репозиторий git с удаленным URL-адресом GitHub. Вот как я это сделал: https://github.com/Habimm/object_detection
Теперь клонируйте репозиторий и выполните следующие действия, чтобы установить веб-службу на экземпляре AWS:
#!/usr/bin/env bash # Stop if one of the below line commands results in an error. set -e sudo apt update sudo apt install -y libgl1-mesa-glx # Install Anaconda3 wget -O Anaconda3.sh https://repo.anaconda.com/archive/Anaconda3-2023.03-1-Linux-x86_64.sh chmod +x Anaconda3.sh rm -rf ~/anaconda3 bash Anaconda3.sh -b echo 'export PATH="$HOME/anaconda3/bin:$PATH"' >> ~/.bashrc . ~/.bashrc . ~/anaconda3/etc/profile.d/conda.sh # Delete a priorly setup Anaconda3 environment conda activate base rm -rf $(conda info --base)/envs/object_detection/ conda env remove --name object_detection # Create a fresh Anaconda3 environment conda create --yes --name object_detection python conda activate object_detection pip install --upgrade --requirement requirements.txt # Copy the SSL certificate + key from the home directory, # (to which we copied them using scp # after creating them with mkcert) cp ../{certificate,key}.pem . # Run the web service, # in such a way that it continues running, # even after the SSH connection terminates. pkill -f "python api.py" nohup python api.py > output.log 2>&1 & ps aux | grep "python api.py" cat output.log
Это горсть!
Все, что мы на самом деле делаем, это следующие пять вещей:
- В начале мы говорим set -e, что заставит остановить оболочку, если одна из команд выдаст ошибку.
- Мы устанавливаем apt-пакет, который может потребоваться для веб-службы.
- Мы устанавливаем Anaconda3, менеджер зависимостей для Python. Например, мы используем Anaconda для установки пакета Python ultralytics, который содержит код для оценки нашей типичной модели YOLOv8.
- Мы копируем SSL сертификат и ключ в репозиторий git.
- Запускаем веб-сервис. Здесь мы используем трюк nohup, который предотвращает закрытие службы, когда мы сами Ctrl+D выходим из SSH-терминала.
Вы можете поместить этот шелл-код в файл с именем INSTALL и также закоммитить его в репозиторий git. Затем вы можете запустить этот файл bash сразу после клонирования репозитория git на экземпляре AWS, в который вы SSH с полученным IP-адресом. Терраформ.
Это очень приятно! После запуска скрипта установки мы можем увидеть что-то вроде следующего:
Теперь мы хотим добавить эти ограничивающие рамки (!) в наше изображение. Но какой IP-адрес мы вводим в Postman? Бегать:
curl ifconfig.me && echo
Это выведет IP-адрес, который мы можем поместить в Postman. Давайте посмотрим:
Это сработало!
Что мы наделали? Мы использовали Terraform для развертывания экземпляра AWS где-то в западной части США. Затем мы загрузили наш код веб-службы YOLOv8 в этот экземпляр и запустили его. Затем мы использовали нашего локального клиента под названием Postman, чтобы отправить изображение 4 красивых женщин в веб-службу для обнаружения объектов на изображении и обрамления вокруг них. И тогда мы фактически получили изображение с граничными прямоугольниками!
Создание пользовательского интерфейса для веб-сервиса
В качестве последнего шага в нашем маленьком путешествии по творчеству давайте создадим пользовательский интерфейс, в котором человек может фактически загрузить изображение и получить эти ограничивающие рамки. Я имею в виду: мы хотим избавиться от этого интерфейса Postman. !
Для этого шага мы будем использовать ReactJS. Давайте перейдем в родительский каталог нашего object_detection/ и запустим:
npx create-react-app detection_interface
Теперь у нас уже есть довольно обширный скелет для написания кода для нашего UI-приложения. Просто откройте detection_interface/src/App.css и замените содержимое на:
img { position: relative; } /*https://stackoverflow.com/questions/22051573/how-to-hide-image-broken-icon-using-only-css-html*/ img:after { display: block; position: absolute; top: 0; width: 100%; height: 100%; background-color: #fff; content: attr(alt); } .App { text-align: center; } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; }
Это окружит изображения желтой рамкой и добавит пару других стилей.
Затем откройте detection_interface/src/App.js:
import './App.css'; import React, { useState } from 'react'; function App() { const [selectedImage, setSelectedImage] = useState(null); const [outlinedImage, setOutlinedImage] = useState(null); const handleImageUpload = (event) => { if (event.target.files.length === 0) { return; } var selectedFileObject = event.target.files[0]; // We use a temporary variable, because // React batches state updates for performance reasons, // so we cannot call setSelectedImage() and expect // selectedImage to hold the correct value in the next line. var tempSelectedImage = URL.createObjectURL(selectedFileObject); fetch(tempSelectedImage) .then(response => response.blob()) .then(blob => { var reader = new FileReader(); reader.onloadend = function() { // could be a: ArrayBuffer(497559) var imageArrayBuffer = reader.result; var myHeaders = { "Content-Type": "image/jpeg" }; var requestOptions = { method: 'POST', headers: myHeaders, body: imageArrayBuffer, redirect: 'follow', }; fetch(process.env.REACT_APP_API_URL, requestOptions) .then(response => response.blob()) .then(blob => { const detectionsBlobUrl = URL.createObjectURL(blob); setOutlinedImage(detectionsBlobUrl); }) .catch(error => console.log('error', error)); } reader.readAsArrayBuffer(blob); }); setSelectedImage(tempSelectedImage); }; return ( <div className="App"> <h3> Upload an image to detect objects! </h3> <header className="App-header"> <input style={{border: '4px solid red'}} type="file" accept="image/*" onChange={handleImageUpload} /> {( <div style={{display: 'flex', justifyContent: 'center'}}> <img src={selectedImage} alt="Selected" style={{width: '100%', height: 'auto', border: '10px solid yellow', maxWidth: '45%'}} /> <img src={outlinedImage} alt="Outlined" style={{width: '100%', height: 'auto', border: '10px solid yellow', maxWidth: '45%'}} /> </div> )} </header> </div> ); } export default App;
Это фактический код пользовательского интерфейса:
- Мы заставляем браузер запрашивать у пользователя файл в виде изображения, если он нажимает кнопку Выбрать файл.
- Затем мы читаем изображение с жесткого диска строкой var imageArrayBuffer = reader.result;
- Затем мы проверяем переменную process.env.REACT_APP_API_URL, в которую помещаем общедоступный IP-адрес экземпляра AWS.
- И мы сделаем запрос к веб-службе экземпляра AWS. Получим результат и используем хук React setOutlinedImage(detectionsBlobUrl);, чтобы запомнить изображение с граничными прямоугольниками.
- Как только экземпляр AWS отправляет ответ HTTPS с изображением с нужными нам граничными рамками, React воссоздает этот HTML-код, чтобы изображение с граничными рамками, которое будет сохранено в оперативная память браузера, будет отображаться пользователю.
Теперь создайте файл detection_interface/.env:
REACT_APP_API_URL=https://34.221.234.90:8080/yolo
Замените цифры общедоступным IP-адресом вашего экземпляра AWS. Затем перейдите в корневой каталог интерфейсного приложения, которым является detection_interface/, и запустите:
npm start
Надеюсь, вы можете получить такой результат, как:
Здесь модель YOLOv8 ведет себя очень забавно, потому что очень многие люди на самом деле вообще не обнаруживаются.
Разверните пользовательский интерфейс
Это очень, очень хорошо! Но на самом деле нам еще предстоит развернуть пользовательский интерфейс для широкой публики. Если только наш единственный пользователь не является соседом, крадущим WiFi, который ранее обнаруживал объекты на изображениях.
Переходим к Netlify!
Единственная причина, по которой я упоминаю Netlify, заключается в том, что с мая 2023 года эта платформа совершенно бесплатна, если вы просто хотите разместить пару строк кода пользовательского интерфейса. Я не совсем уверен в AWS в этом вопросе.
Но, помимо затрат, платформа Netlify, на мой взгляд, намного удобнее для пользователя, чем платформа AWS. Это очень чистый и простой дизайн, который действительно оживляет.
Итак, давайте сначала отправим этот код пользовательского интерфейса на GitHub: создайте новый репозиторий GitHub, инициализируйте репозиторий git в своем каталоге detection_interface/, зафиксируйте и загрузите все на GitHub.
Одно маленькое исключение: не отправляйте файл .env. Такие файлы никогда не должны возвращаться в репозитории git.
Вот мой GitHub: https://github.com/Habimm/detection-interface
Теперь перейдите в Netlify и нажмите
Add new site -> Import an existing project -> GitHub
Затем выберите репозиторий GitHub. Это довольно упрощенный процесс публикации кода из вашего репозитория GitHub.
Как только Netlify развернет ваш пользовательский интерфейс, у вас может возникнуть соблазн загрузить изображение:
Тем не менее, никакой реакции…
Почему это? Помните тот файл .env, о котором я предупреждал вас не включать. Вот и причина: файл содержал IP-адрес экземпляра AWS. И теперь нам нужно найти другой способ сообщить Netlify IP-адрес экземпляра AWS.
Для этого перейдите на
Site overview -> Site settings -> Environment variables -> Add a variable -> Import from a .env file
Откройте этот ваш файл .env (который будет содержать только одну строку). Скопируйте все из него в окно Netlify. Импортировать переменные.
Теперь, чтобы Netlify заменил
process.env.REACT_APP_API_URL
часть вашего кода с
https://35.92.22.120:8080/yolo
нам нужно повторно развернуть пользовательский интерфейс. В левой части панели инструментов Netlify нажмите
Deploys -> Trigger deploy -> Deploy site
Затем подождите:
Роботы Netlify заняты созданием и развертыванием вашего сайта в нашей CDN.
Затем снова откройте Обзор сайта и подождите, пока текст Последняя публикация в 21:45 не покажет текущее время. Заходим внутрь и вуаля:
Вот оно, народ!
Что мы наделали?
Сегодня мы развернули компьютерную программу 4 раза! Мы развернули в нашей локальной системе веб-сервис для обнаружения объектов на изображениях. Мы развернули тот же веб-сервис на экземпляре AWS, который мы настроили с помощью Terraform. Мы развернули пользовательский интерфейс, созданный с помощью ReactJS, в нашей локальной системе. И, наконец, мы развернули пользовательский интерфейс в Netlify, изменив переменную среды с помощью удобной панели инструментов Netlify!
Ух ты! Я действительно горжусь тобой. Не могу поверить, ты все еще со мной! Если вы сделали это здесь, я совершенно потерял дар речи.
Вот и все!
Теперь идите и разверните свой собственный проект машинного обучения!
И расскажите об этом :)