MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ืฉืœื•ื ืœื›ื•ืœื! ืื ื™ ืžืคืชื— ืงื•ืจื•ืช ื—ื™ื™ื ื‘-CROC. ืื ื• ืžื™ื™ืฉืžื™ื ืคืจื•ื™ืงื˜ื™ื ื‘ืชื—ื•ื ืงื•ืจื•ืช ื”ื—ื™ื™ื ื›ื‘ืจ 3 ืฉื ื™ื. ื‘ื–ืžืŸ ื”ื–ื” ืขืฉื™ื ื• ื”ืจื‘ื” ื“ื‘ืจื™ื, ืœืžืฉืœ: ืขืงื‘ื ื• ืื—ืจื™ ื ื”ื’ื™ื ื›ื“ื™ ืฉื‘ื–ืžืŸ ื”ื ื”ื™ื’ื” ื”ื ืœื ืฉืชื•, ืœื ืขื™ืฉื ื•, ืœื ื“ื™ื‘ืจื• ื‘ื˜ืœืคื•ืŸ, ื”ืกืชื›ืœื• ืขืœ ื”ื›ื‘ื™ืฉ, ื•ืœื ืขืœ ื—ืœื•ืžื•ืช ืื• ืขืœ ื”ืขื ื ื™ื. ; ื”ืงืœื˜ื ื• ืื ืฉื™ื ืฉื ื•ืกืขื™ื ื‘ื ืชื™ื‘ื™ื ื™ื™ืขื•ื“ื™ื™ื ื•ืชื•ืคืกื™ื ืžืกืคืจ ืžืงื•ืžื•ืช ื—ื ื™ื”; ื“ืื’ ืœื›ืš ืฉื”ืขื•ื‘ื“ื™ื ื™ื—ื‘ืฉื• ืงืกื“ื•ืช, ื›ืคืคื•ืช ื•ื›ื•'; ื–ื™ื”ื” ืขื•ื‘ื“ ืฉืจื•ืฆื” ืœื”ื™ื›ื ืก ืœืžืชืงืŸ; ืกืคืจื ื• ื›ืœ ืžื” ืฉื™ื›ื•ืœื ื•.

ื‘ืฉื‘ื™ืœ ืžื” ืื ื™ ืขื•ืฉื” ืืช ื›ืœ ื–ื”?

ื‘ืชื”ืœื™ืš ื‘ื™ืฆื•ืข ื”ืคืจื•ื™ืงื˜ื™ื ืคื’ืขื ื• ื‘ืžื”ืžื•ืจื•ืช, ื”ืจื‘ื” ืžื”ืžื•ืจื•ืช, ื—ืœืง ืžื”ื‘ืขื™ื•ืช ืฉืืชื” ืžื›ื™ืจ ืื• ืฉืชื›ื™ืจ ื‘ืขืชื™ื“.

ื“ื•ื’ืžื ื•ืช ื”ืžืฆื‘

ื‘ื•ืื• ื ื“ืžื™ื™ืŸ ืฉืงื™ื‘ืœื ื• ืขื‘ื•ื“ื” ื‘ื—ื‘ืจื” ืฆืขื™ืจื” "N", ืฉืคืขื™ืœื•ืชื” ืงืฉื•ืจื” ืœ-ML. ืื ื—ื ื• ืขื•ื‘ื“ื™ื ืขืœ ืคืจื•ื™ืงื˜ ML (DL, CV), ื•ืื– ืžืกื™ื‘ื” ื›ืœืฉื”ื™ ืื ื—ื ื• ืขื•ื‘ืจื™ื ืœืขื‘ื•ื“ื” ืื—ืจืช, ื‘ื“ืจืš ื›ืœืœ ืœื•ืงื—ื™ื ื”ืคืกืงื” ื•ื—ื•ื–ืจื™ื ืœื ื•ื™ืจื•ืŸ ืฉืœื ื• ืื• ืฉืœ ืžื™ืฉื”ื• ืื—ืจ.

  1. ืจื’ืข ื”ืืžืช ืžื’ื™ืข, ืืชื” ืฆืจื™ืš ืื™ื›ืฉื”ื• ืœื–ื›ื•ืจ ืื™ืคื” ืขืฆืจืช, ืœืื™ืœื• ื”ื™ืคืจืคืจืžื˜ืจื™ื ื ื™ืกื™ืช ื•ื‘ืขื™ืงืจ, ืœืื™ืœื• ืชื•ืฆืื•ืช ื”ื ื”ื•ื‘ื™ืœื•. ื™ื›ื•ืœื•ืช ืœื”ื™ื•ืช ืืคืฉืจื•ื™ื•ืช ืจื‘ื•ืช ืœืžื™ ืฉืžืจ ืžื™ื“ืข ื‘ื›ืœ ื”ื”ืฉืงื•ืช: ื‘ืจืืฉ, ื‘ืชืฆื•ืจื•ืช, ื‘ืคื ืงืก ืจืฉื™ืžื•ืช, ื‘ืกื‘ื™ื‘ืช ืขื‘ื•ื“ื” ื‘ืขื ืŸ. ื‘ืžืงืจื” ืจืื™ืชื™ ืืคืฉืจื•ืช ื›ืืฉืจ ื”ื™ืคืจืคืจืžื˜ืจื™ื ืื•ื—ืกื ื• ื›ืฉื•ืจื•ืช ื”ืขืจื•ืช ื‘ืงื•ื“, ื‘ืื•ืคืŸ ื›ืœืœื™, ืžืจืื” ืžืคื•ืืจ. ืขื›ืฉื™ื• ื“ืžื™ื™ื ื• ืฉื—ื–ืจืชื ืœื ืœืคืจื•ื™ืงื˜ ืฉืœื›ื, ืืœื ืœืคืจื•ื™ืงื˜ ืฉืœ ืื“ื ืฉืขื–ื‘ ืืช ื”ื—ื‘ืจื” ื•ื™ืจืฉืชื ืงื•ื“ ื•ืžื•ื“ืœ ื‘ืฉื model_1.pb. ื›ื“ื™ ืœื”ืฉืœื™ื ืืช ื”ืชืžื•ื ื” ื•ืœื”ืขื‘ื™ืจ ืืช ื›ืœ ื”ื›ืื‘, ื‘ื•ืื• ื ื“ืžื™ื™ืŸ ืฉืืชื” ื’ื ืžื•ืžื—ื” ืžืชื—ื™ืœ.
  2. ืœืš ืขืœ ื–ื”. ื›ื“ื™ ืœื”ืคืขื™ืœ ืืช ื”ืงื•ื“, ืื ื—ื ื• ื•ื›ืœ ืžื™ ืฉื™ืขื‘ื•ื“ ืื™ืชื• ืฆืจื™ื›ื™ื ืœื™ืฆื•ืจ ืกื‘ื™ื‘ื”. ืœืขืชื™ื ืงืจื•ื‘ื•ืช ืงื•ืจื” ืฉืžืฉื•ื ืžื” ืœื ื”ืฉืื™ืจื• ืื•ืชื• ื‘ืชื•ืจ ื”ื™ืจื•ืฉื” ืฉืœื ื•. ื–ื• ื™ื›ื•ืœื” ื’ื ืœื”ืคื•ืš ืœืžืฉื™ืžื” ืœื ื˜ืจื™ื•ื•ื™ืืœื™ืช. ืืชื” ืœื ืจื•ืฆื” ืœื‘ื–ื‘ื– ื–ืžืŸ ืขืœ ื”ืฆืขื“ ื”ื–ื”, ื ื›ื•ืŸ?
  3. ืื ื• ืžืืžื ื™ื ื“ื’ื (ืœืžืฉืœ, ื’ืœืื™ ืจื›ื‘). ื”ื’ืขื ื• ืœื ืงื•ื“ื” ืฉื‘ื” ื–ื” ื”ื•ืคืš ืœื”ื™ื•ืช ื˜ื•ื‘ ืžืื•ื“ - ื”ื’ื™ืข ื”ื–ืžืŸ ืœืฉืžื•ืจ ืืช ื”ืชื•ืฆืื”. ื‘ื•ืื• ื ืงืจื ืœื–ื” car_detection_v1.pb. ืœืื—ืจ ืžื›ืŸ ืื ื• ืžืืžื ื™ื ืขื•ื“ ืื—ื“ - car_detection_v2.pb. ื–ืžืŸ ืžื” ืœืื—ืจ ืžื›ืŸ, ื”ืงื•ืœื’ื•ืช ืฉืœื ื• ืื• ืื ื—ื ื• ื‘ืขืฆืžื ื• ืžืœืžื“ื™ื ื™ื•ืชืจ ื•ื™ื•ืชืจ, ืชื•ืš ืฉื™ืžื•ืฉ ื‘ืืจื›ื™ื˜ืงื˜ื•ืจื•ืช ืฉื•ื ื•ืช. ื›ืชื•ืฆืื” ืžื›ืš ื ื•ืฆืจืช ื—ื‘ื•ืจื” ืฉืœ ื—ืคืฆื™ื, ืฉืขืœื™ื”ื ื™ืฉ ืœืืกื•ืฃ ืžื™ื“ืข ื‘ืงืคื™ื“ื” (ืื‘ืœ ื ืขืฉื” ื–ืืช ืžืื•ื—ืจ ื™ื•ืชืจ, ื›ื™ ืœืขืช ืขืชื” ื™ืฉ ืœื ื• ืขื ื™ื™ื ื™ื ืขื“ื™ืคื•ืช ื™ื•ืชืจ).
  4. ื‘ืกื“ืจ ื”ื›ืœ ื ื’ืžืจ ืขื›ืฉื™ื•! ื™ืฉ ืœื ื• ื“ื’ื! ื”ืื ื ื•ื›ืœ ืœื”ืชื—ื™ืœ ืœืืžืŸ ืืช ื”ืžื•ื“ืœ ื”ื‘ื, ืœืคืชื— ืืจื›ื™ื˜ืงื˜ื•ืจื” ืœืคืชืจื•ืŸ ื‘ืขื™ื” ื—ื“ืฉื”, ืื• ืฉื ื•ื›ืœ ืœืœื›ืช ืœืฉืชื•ืช ืชื”? ื•ืžื™ ื™ืคืจืก?

ื–ื™ื”ื•ื™ ื‘ืขื™ื•ืช

ืขื‘ื•ื“ื” ืขืœ ืคืจื•ื™ืงื˜ ืื• ืžื•ืฆืจ ื”ื™ื ืขื‘ื•ื“ื” ืฉืœ ืื ืฉื™ื ืจื‘ื™ื. ื•ืขื ื”ื–ืžืŸ ืื ืฉื™ื ืขื•ื–ื‘ื™ื ื•ื‘ืื™ื, ื™ืฉ ื™ื•ืชืจ ืคืจื•ื™ืงื˜ื™ื, ื•ื”ืคืจื•ื™ืงื˜ื™ื ืขืฆืžื ื”ื•ืคื›ื™ื ืžื•ืจื›ื‘ื™ื ื™ื•ืชืจ. ื›ืš ืื• ืื—ืจืช, ืžืฆื‘ื™ื ืžื”ืžื—ื–ื•ืจ ื”ืžืชื•ืืจ ืœืขื™ืœ (ื•ืœื ืจืง) ื‘ืฉื™ืœื•ื‘ื™ื ืžืกื•ื™ืžื™ื ื™ืชืจื—ืฉื• ืžืื™ื˜ืจืฆื™ื” ืœืื™ื˜ืจืฆื™ื”. ื›ืœ ื–ื” ื’ื•ืจื ืœื‘ื–ื‘ื•ื– ื–ืžืŸ, ื‘ืœื‘ื•ืœ, ืขืฆื‘ื™ื, ืื•ืœื™ ื—ื•ืกืจ ืฉื‘ื™ืขื•ืช ืจืฆื•ืŸ ืฉืœ ืœืงื•ื—ื•ืช, ื•ื‘ืกื•ืคื• ืฉืœ ื“ื‘ืจ, ื›ืกืฃ ืื‘ื•ื“. ืœืžืจื•ืช ืฉื›ื•ืœื ื• ื‘ื“ืจืš ื›ืœืœ ืขื•ืงื‘ื™ื ืื—ืจ ืื•ืชื” ืžื’ืจืคื” ื™ืฉื ื”, โ€‹โ€‹ืื ื™ ืžืืžื™ืŸ ืฉืืฃ ืื—ื“ ืœื ืจื•ืฆื” ืœืฉื—ื–ืจ ืืช ื”ืจื’ืขื™ื ื”ืืœื” ืฉื•ื‘ ื•ืฉื•ื‘.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ืื–, ืขื‘ืจื ื• ืžื—ื–ื•ืจ ืคื™ืชื•ื— ืื—ื“ ื•ืื ื—ื ื• ืจื•ืื™ื ืฉื™ืฉ ื‘ืขื™ื•ืช ืฉืฆืจื™ืš ืœืคืชื•ืจ. ื›ื“ื™ ืœืขืฉื•ืช ื–ืืช ืืชื” ืฆืจื™ืš:

  • ืื—ืกืŸ ื‘ืฆื•ืจื” ื ื•ื—ื” ืชื•ืฆืื•ืช ืขื‘ื•ื“ื”;
  • ืœื”ืคื•ืš ืืช ืชื”ืœื™ืš ืฉื™ืชื•ืฃ ื”ืขื•ื‘ื“ื™ื ื”ื—ื“ืฉื™ื ืœืคืฉื•ื˜;
  • ืœืคืฉื˜ ืืช ืชื”ืœื™ืš ืคืจื™ืกืช ืกื‘ื™ื‘ืช ืคื™ืชื•ื—;
  • ืœื”ื’ื“ื™ืจ ืืช ืชื”ืœื™ืš ื’ื™ืจืกืช ื”ืžื•ื“ืœ;
  • ื™ืฉ ื“ืจืš ื ื•ื—ื” ืœืืžืช ืžื•ื“ืœื™ื;
  • ืœืžืฆื•ื ื›ืœื™ ืœื ื™ื”ื•ืœ ืžืฆื‘ ืžื•ื“ืœ;
  • ืœืžืฆื•ื ื“ืจืš ืœื”ืขื‘ื™ืจ ื“ื’ืžื™ื ืœื™ื™ืฆื•ืจ.

ื›ื ืจืื” ืฉื™ืฉ ืฆื•ืจืš ืœื”ืžืฆื™ื ื–ืจื™ืžืช ืขื‘ื•ื“ื” ืฉืชืืคืฉืจ ืœืš ืœื ื”ืœ ื‘ืงืœื•ืช ื•ื‘ื ื•ื—ื•ืช ืืช ืžื—ื–ื•ืจ ื”ื—ื™ื™ื ื”ื–ื”? ืชืจื’ื•ืœ ื–ื” ื ืงืจื MLOps

MLOps, ืื• DevOps ืœืœืžื™ื“ืช ืžื›ื•ื ื”, ืžืืคืฉืจืช ืœืฆื•ื•ืชื™ ืžื“ืขื™ ื”ื ืชื•ื ื™ื ื•-IT ืœืฉืชืฃ ืคืขื•ืœื” ื•ืœื”ื’ื‘ื™ืจ ืืช ืงืฆื‘ ื”ืคื™ืชื•ื— ื•ื”ืคืจื™ืกื” ืฉืœ ื”ืžื•ื“ืœื™ื ื‘ืืžืฆืขื•ืช ื ื™ื˜ื•ืจ, ืื™ืžื•ืช ื•ืžืžืฉืœ ืขื‘ื•ืจ ืžื•ื“ืœื™ื ืฉืœ ืœืžื™ื“ืช ืžื›ื•ื ื”.

ืืชื” ื™ื›ื•ืœ ืœืงืจื•ืืžื” ื—ื•ืฉื‘ื™ื ื”ื—ื‘ืจ'ื” ืฉืœ ื’ื•ื’ืœ ืขืœ โ€‹โ€‹ื›ืœ ื–ื”? ืžื”ืžืืžืจ ื‘ืจื•ืจ ืฉ-MLOps ื”ื•ื ื“ื‘ืจ ื“ื™ ื ืคื—.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื‘ื”ืžืฉืš ื”ืžืืžืจ ืฉืœื™ ืืชืืจ ืจืง ื—ืœืง ืžื”ืชื”ืœื™ืš. ืœืฆื•ืจืš ื”ื˜ืžืขื”, ืืฉืชืžืฉ ื‘ื›ืœื™ MLflow, ื›ื™... ื–ื”ื• ืคืจื•ื™ืงื˜ ื‘ืงื•ื“ ืคืชื•ื—, ื ื“ืจืฉืช ื›ืžื•ืช ืงื˜ื ื” ืฉืœ ืงื•ื“ ื›ื“ื™ ืœื”ืชื—ื‘ืจ ื•ื™ืฉ ืื™ื ื˜ื’ืจืฆื™ื” ืขื ืžืกื’ืจื•ืช ml ืคื•ืคื•ืœืจื™ื•ืช. ืืชื” ื™ื›ื•ืœ ืœื—ืคืฉ ื‘ืื™ื ื˜ืจื ื˜ ื›ืœื™ื ืื—ืจื™ื ื›ืžื• Kubeflow, SageMaker, Trains ื•ื›ื•' ื•ืื•ืœื™ ืœืžืฆื•ื ืื—ื“ ืฉืžืชืื™ื ื™ื•ืชืจ ืœืฆืจื›ื™ื ืฉืœืš.

"ื‘ื ื™ื™ืช" MLOps ื‘ืืžืฆืขื•ืช ื”ื“ื•ื’ืžื” ืฉืœ ืฉื™ืžื•ืฉ ื‘ื›ืœื™ MLFlow

MLFlow ื”ื™ื ืคืœื˜ืคื•ืจืžืช ืงื•ื“ ืคืชื•ื— ืœื ื™ื”ื•ืœ ืžื—ื–ื•ืจ ื”ื—ื™ื™ื ืฉืœ ื“ื’ืžื™ ml (https://mlflow.org/).

MLflow ื›ื•ืœืœ ืืจื‘ืขื” ืžืจื›ื™ื‘ื™ื:

  • MLflow Tracking - ืžื›ืกื” ืืช ื”ื‘ืขื™ื•ืช ืฉืœ ืจื™ืฉื•ื ืชื•ืฆืื•ืช ื•ืคืจืžื˜ืจื™ื ืฉื”ื•ื‘ื™ืœื• ืœืชื•ืฆืื” ื–ื•;
  • MLflow Project - ืžืืคืฉืจ ืœืืจื•ื– ืงื•ื“ ื•ืœืฉื›ืคืœ ืื•ืชื• ื‘ื›ืœ ืคืœื˜ืคื•ืจืžื”;
  • MLflow Models - ืื—ืจืื™ ืขืœ ืคืจื™ืกืช ืžื•ื“ืœื™ื ืœื™ื™ืฆื•ืจ;
  • MLflow Registry - ืžืืคืฉืจ ืœืš ืœืื—ืกืŸ ืžื•ื“ืœื™ื ื•ืœื ื”ืœ ืืช ืžืฆื‘ื ื‘ืžืื’ืจ ืžืจื›ื–ื™.

MLflow ืคื•ืขืœืช ืขืœ ืฉืชื™ ื™ืฉื•ื™ื•ืช:

  • ื”ืฉืงื” ื”ื™ื ืžื—ื–ื•ืจ ืฉืœื ืฉืœ ืื™ืžื•ื ื™ื, ืคืจืžื˜ืจื™ื ื•ืžื“ื“ื™ื ืฉืœืคื™ื”ื ืื ื• ืจื•ืฆื™ื ืœื”ื™ืจืฉื;
  • ื ื™ืกื•ื™ ื”ื•ื "ื ื•ืฉื" ืฉืคื•ืขืœ ื‘ื™ื—ื“.

ื›ืœ ื”ืฉืœื‘ื™ื ืฉืœ ื”ื“ื•ื’ืžื” ืžื™ื•ืฉืžื™ื ื‘ืžืขืจื›ืช ื”ื”ืคืขืœื” ืื•ื‘ื•ื ื˜ื• 18.04.

1. ืคืจื•ืก ืืช ื”ืฉืจืช

ืขืœ ืžื ืช ืฉื ื•ื›ืœ ืœื ื”ืœ ื‘ืงืœื•ืช ืืช ื”ืคืจื•ื™ืงื˜ ืฉืœื ื• ื•ืœืงื‘ืœ ืืช ื›ืœ ื”ืžื™ื“ืข ื”ื“ืจื•ืฉ, ื ืคืจื•ืก ืฉืจืช. ืœืฉืจืช ืžืขืงื‘ MLflow ืฉื ื™ ืžืจื›ื™ื‘ื™ื ืขื™ืงืจื™ื™ื:

  • backend store - ืื—ืจืื™ ืขืœ ืื—ืกื•ืŸ ืžื™ื“ืข ืขืœ ื“ื’ืžื™ื ืจืฉื•ืžื™ื (ืชื•ืžืš ื‘-4 DBMSs: mysql, mssql, sqlite ื•-postgresql);
  • ื—ื ื•ืช artifact - ืื—ืจืื™ืช ืขืœ ืื—ืกื•ืŸ ื—ืคืฆื™ื (ืชื•ืžืš ื‘-7 ืืคืฉืจื•ื™ื•ืช ืื—ืกื•ืŸ: Amazon S3, Azure Blob Storage, Google Cloud Storage, ืฉืจืช FTP, SFTP Server, NFS, HDFS).

ื› ื—ื ื•ืช ื—ืคืฆื™ื ืœืฉื ื”ืคืฉื˜ื•ืช, ื‘ื•ืื• ื ื™ืงื— ืฉืจืช sftp.

  • ืœื™ืฆื•ืจ ืงื‘ื•ืฆื”
    $ sudo groupadd sftpg
  • ื”ื•ืกืฃ ืžืฉืชืžืฉ ื•ื”ื’ื“ืจ ืœื• ืกื™ืกืžื”
    $ sudo useradd -g sftpg mlflowsftp
    $ sudo passwd mlflowsftp 
  • ื”ืชืืžืช ื›ืžื” ื”ื’ื“ืจื•ืช ื’ื™ืฉื”
    $ sudo mkdir -p /data/mlflowsftp/upload
    $ sudo chown -R root.sftpg /data/mlflowsftp
    $ sudo chown -R mlflowsftp.sftpg /data/mlflowsftp/upload
  • ื”ื•ืกืฃ ื›ืžื” ืฉื•ืจื•ืช ืœ-/etc/ssh/sshd_config
    Match Group sftpg
     ChrootDirectory /data/%u
     ForceCommand internal-sftp
  • ืœื”ืคืขื™ืœ ืžื—ื“ืฉ ืืช ื”ืฉื™ืจื•ืช
    $ sudo systemctl restart sshd

ื› ื—ื ื•ืช ืขื•ืจืคื™ืช ื‘ื•ืื• ื ื™ืงื— ืืช postgresql.

$ sudo apt update
$ sudo apt-get install -y postgresql postgresql-contrib postgresql-server-dev-all
$ sudo apt install gcc
$ pip install psycopg2
$ sudo -u postgres -i
# Create new user: mlflow_user
[postgres@user_name~]$ createuser --interactive -P
Enter name of role to add: mlflow_user
Enter password for new role: mlflow
Enter it again: mlflow
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
# Create database mlflow_bd owned by mlflow_user
$ createdb -O mlflow_user mlflow_db

ื›ื“ื™ ืœื”ืคืขื™ืœ ืืช ื”ืฉืจืช, ืขืœื™ืš ืœื”ืชืงื™ืŸ ืืช ื—ื‘ื™ืœื•ืช ื”ืคื™ืชื•ืŸ ื”ื‘ืื•ืช (ืื ื™ ืžืžืœื™ืฅ ืœื™ืฆื•ืจ ืกื‘ื™ื‘ื” ื•ื™ืจื˜ื•ืืœื™ืช ื ืคืจื“ืช):

pip install mlflow
pip install pysftp

ื‘ื•ืื• ื ืชื—ื™ืœ ืืช ื”ืฉืจืช ืฉืœื ื•

$ mlflow server  
                 --backend-store-uri postgresql://mlflow_user:mlflow@localhost/mlflow_db 
                 --default-artifact-root sftp://mlflowsftp:mlflow@sftp_host/upload  
                --host server_host 
                --port server_port

2. ื”ื•ืกืฃ ืžืขืงื‘

ื›ื“ื™ ืฉืชื•ืฆืื•ืช ื”ื”ื“ืจื›ื” ืฉืœื ื• ืœื ื™ืื‘ื“ื•, ื“ื•ืจื•ืช ื”ืขืชื™ื“ ืฉืœ ืžืคืชื—ื™ื ื™ื‘ื™ื ื• ืžื” ืงื•ืจื”, ื•ื›ื“ื™ ืฉื—ื‘ืจื™ื ืžื‘ื•ื’ืจื™ื ื•ืืชื ืชื•ื›ืœื• ืœื ืชื— ื‘ืจื•ื’ืข ืืช ืชื”ืœื™ืš ื”ืœืžื™ื“ื”, ืขืœื™ื ื• ืœื”ื•ืกื™ืฃ ืžืขืงื‘. ืžืขืงื‘ ืžืฉืžืขื• ืฉืžื™ืจืช ืคืจืžื˜ืจื™ื, ืžื“ื“ื™ื, ื—ืคืฆื™ื ื•ื›ืœ ืžื™ื“ืข ื ื•ืกืฃ ืขืœ ืชื—ื™ืœืช ื”ืื™ืžื•ืŸ, ื‘ืžืงืจื” ืฉืœื ื•, ื‘ืฉืจืช.

ืœื“ื•ื’ืžื”, ื™ืฆืจืชื™ ืงื˜ืŸ ืคืจื•ื™ืงื˜ ื‘-github ื‘-Keras ืœืคื™ืœื•ื— ื›ืœ ืžื” ืฉื ืžืฆื ื‘ืคื ื™ื ืžืขืจืš ื ืชื•ื ื™ื ืฉืœ COCO. ื›ื“ื™ ืœื”ื•ืกื™ืฃ ืžืขืงื‘, ื™ืฆืจืชื™ ืงื•ื‘ืฅ mlflow_training.py.

ืœื”ืœืŸ ื”ืฉื•ืจื•ืช ืฉื‘ื”ืŸ ืงื•ืจื™ื ื”ื“ื‘ืจื™ื ื”ืžืขื ื™ื™ื ื™ื ื‘ื™ื•ืชืจ:

def run(self, epochs, lr, experiment_name):
        # getting the id of the experiment, creating an experiment in its absence
        remote_experiment_id = self.remote_server.get_experiment_id(name=experiment_name)
        # creating a "run" and getting its id
        remote_run_id = self.remote_server.get_run_id(remote_experiment_id)

        # indicate that we want to save the results on a remote server
        mlflow.set_tracking_uri(self.tracking_uri)
        mlflow.set_experiment(experiment_name)

        with mlflow.start_run(run_id=remote_run_id, nested=False):
            mlflow.keras.autolog()
            self.train_pipeline.train(lr=lr, epochs=epochs)

        try:
            self.log_tags_and_params(remote_run_id)
        except mlflow.exceptions.RestException as e:
            print(e)

ื›ืืŸ self.remote_server ื”ื•ื ืžืขื˜ืคืช ืงื˜ื ื” ืžืขืœ ืฉื™ื˜ื•ืช mlflow.tracking. MlflowClient (ื”ื›ื ืชื™ ืื•ืชื• ืžื˜ืขืžื™ ื ื•ื—ื•ืช), ื‘ืขื–ืจืชื• ืื ื™ ื™ื•ืฆืจ ื ื™ืกื•ื™ ื•ืžืคืขื™ืœ ืื•ืชื• ืขืœ ื”ืฉืจืช. ืœืื—ืจ ืžื›ืŸ, ืื ื™ ืžืฆื™ื™ืŸ ื”ื™ื›ืŸ ื™ืฉ ืœืžื–ื’ ืืช ืชื•ืฆืื•ืช ื”ื”ืฉืงื” (mlflow.set_tracking_uri(self.tracking_uri)). ืื ื™ ืžืืคืฉืจ ืจื™ืฉื•ื ืื•ื˜ื•ืžื˜ื™ ืฉืœ mlflow.keras.autolog(). ื ื›ื•ืŸ ืœืขื›ืฉื™ื• MLflow Tracking ืชื•ืžืš ื‘ืจื™ืฉื•ื ืื•ื˜ื•ืžื˜ื™ ืขื‘ื•ืจ TensorFlow, Keras, Gluon XGBoost, LightGBM, Spark. ืื ืœื ืžืฆืืช ืืช ื”ืžืกื’ืจืช ืื• ื”ืกืคืจื™ื™ื” ืฉืœืš, ืื– ืืชื” ืชืžื™ื“ ื™ื›ื•ืœ ืœื”ื™ื›ื ืก ื‘ืื•ืคืŸ ืžืคื•ืจืฉ. ืื ื—ื ื• ืžืชื—ื™ืœื™ื ืœื”ืชืืžืŸ. ืจื™ืฉื•ื ืชื’ื™ื•ืช ื•ืคืจืžื˜ืจื™ ืงืœื˜ ื‘ืฉืจืช ื”ืžืจื•ื—ืง.

ื›ืžื” ืฉื•ืจื•ืช ื•ืœืš, ื›ืžื• ืœื›ื•ืœื, ื™ืฉ ื’ื™ืฉื” ืœืžื™ื“ืข ืขืœ ื›ืœ ื”ื”ืฉืงื•ืช. ืžื’ื ื™ื‘?

3. ืื ื• ืžืชื•ื•ื™ื ืืช ื”ืคืจื•ื™ืงื˜

ืขื›ืฉื™ื• ื‘ื•ืื• ื ืงืœ ืขืœ ื”ืฉืงืช ื”ืคืจื•ื™ืงื˜. ืœืฉื ื›ืš, ื”ื•ืกืฃ ืืช ื”ืงื•ื‘ืฅ MLproject ื•-conda.yaml ืœืฉื•ืจืฉ ื”ืคืจื•ื™ืงื˜.
MLproject

name: flow_segmentation
conda_env: conda.yaml

entry_points:
  main:
    parameters:
        categories: {help: 'list of categories from coco dataset'}
        epochs: {type: int, help: 'number of epochs in training'}

        lr: {type: float, default: 0.001, help: 'learning rate'}
        batch_size: {type: int, default: 8}
        model_name: {type: str, default: 'Unet', help: 'Unet, PSPNet, Linknet, FPN'}
        backbone_name: {type: str, default: 'resnet18', help: 'exampe resnet18, resnet50, mobilenetv2 ...'}

        tracking_uri: {type: str, help: 'the server address'}
        experiment_name: {type: str, default: 'My_experiment', help: 'remote and local experiment name'}
    command: "python mlflow_training.py 
            --epochs={epochs}
            --categories={categories}
            --lr={lr}
            --tracking_uri={tracking_uri}
            --model_name={model_name}
            --backbone_name={backbone_name}
            --batch_size={batch_size}
            --experiment_name={experiment_name}"

ืœืคืจื•ื™ืงื˜ MLflow ืžืกืคืจ ืžืืคื™ื™ื ื™ื:

  • ืฉื - ืฉื ื”ืคืจื•ื™ืงื˜ ืฉืœืš;
  • ืกื‘ื™ื‘ื” - ื‘ืžืงืจื” ืฉืœื™, conda_env ืžืฆื™ื™ืŸ ืฉ-Anaconda ืžืฉืžืฉ ืœื”ืคืขืœื” ื•ืชื™ืื•ืจ ื”ืชืœื•ืช ื ืžืฆื ื‘ืงื•ื‘ืฅ conda.yaml;
  • ื ืงื•ื“ื•ืช ื›ื ื™ืกื” - ืžืฆื™ื™ื ืช ืื™ืœื• ืงื‘ืฆื™ื ื•ืขื ืื™ืœื• ืคืจืžื˜ืจื™ื ื ื•ื›ืœ ืœื”ืคืขื™ืœ (ื›ืœ ื”ืคืจืžื˜ืจื™ื ื ืจืฉืžื™ื ืื•ื˜ื•ืžื˜ื™ืช ืขื ืชื—ื™ืœืช ื”ืื™ืžื•ืŸ)

conda.yaml

name: flow_segmentation
channels:
  - defaults
  - anaconda
dependencies:
  - python==3.7
  - pip:
    - mlflow==1.8.0
    - pysftp==0.2.9
    - Cython==0.29.19
    - numpy==1.18.4
    - pycocotools==2.0.0
    - requests==2.23.0
    - matplotlib==3.2.1
    - segmentation-models==1.0.1
    - Keras==2.3.1
    - imgaug==0.4.0
    - tqdm==4.46.0
    - tensorflow-gpu==1.14.0

ืืชื” ื™ื›ื•ืœ ืœื”ืฉืชืžืฉ ื‘-docer ื›ืกื‘ื™ื‘ืช ื–ืžืŸ ื”ืจื™ืฆื” ืฉืœืš, ืœืคืจื˜ื™ื ื ื•ืกืคื™ื ืขื™ื™ืŸ ืชื™ืขื•ื“.

4. ื‘ื•ืื• ื ืชื—ื™ืœ ืœื”ืชืืžืŸ

ืื ื• ืžืฉื›ืคืœื™ื ืืช ื”ืคืจื•ื™ืงื˜ ื•ื ื›ื ืกื™ื ืœืกืคืจื™ื™ืช ื”ืคืจื•ื™ืงื˜:

git clone https://github.com/simbakot/mlflow_example.git
cd mlflow_example/

ื›ื“ื™ ืœื”ืคืขื™ืœ ืืชื” ืฆืจื™ืš ืœื”ืชืงื™ืŸ ืกืคืจื™ื•ืช

pip install mlflow
pip install pysftp

ื›ื™ ื‘ื“ื•ื’ืžื” ืฉืื ื™ ืžืฉืชืžืฉ ื‘-conda_env, Anaconda ื—ื™ื™ื‘ืช ืœื”ื™ื•ืช ืžื•ืชืงื ืช ื‘ืžื—ืฉื‘ ืฉืœืš (ืื‘ืœ ืืชื” ื™ื›ื•ืœ ืœืขืงื•ืฃ ืืช ื–ื” ืขืœ ื™ื“ื™ ื”ืชืงื ืช ื›ืœ ื”ื—ื‘ื™ืœื•ืช ื”ื“ืจื•ืฉื•ืช ื‘ืขืฆืžืš ื•ืžืฉื—ืง ืขื ืคืจืžื˜ืจื™ ื”ื”ืฉืงื”).

ื›ืœ ืฉืœื‘ื™ ื”ื”ื›ื ื” ื”ื•ืฉืœืžื• ื•ื ื•ื›ืœ ืœื”ืชื—ื™ืœ ืœื”ืฉื™ืง ืืช ื”ื”ื“ืจื›ื”. ืžืฉื•ืจืฉ ื”ืคืจื•ื™ืงื˜:

$ mlflow run -P epochs=10 -P categories=cat,dog -P tracking_uri=http://server_host:server_port .

ืœืื—ืจ ื”ื–ื ืช ื”ืคืงื•ื“ื”, ืกื‘ื™ื‘ืช ืงื•ื ื“ื” ืชื™ื•ื•ืฆืจ ืื•ื˜ื•ืžื˜ื™ืช ื•ื”ืื™ืžื•ืŸ ื™ืชื—ื™ืœ.
ื‘ื“ื•ื’ืžื” ืœืžืขืœื” ื”ืขื‘ืจืชื™ ืืช ืžืกืคืจ ื”ืขื™ื“ื ื™ื ืœืื™ืžื•ืŸ, ื”ืงื˜ื’ื•ืจื™ื•ืช ืืœื™ื”ืŸ ื ืจืฆื” ืœืคืœื— (ืชื•ื›ืœื• ืœืจืื•ืช ืืช ื”ืจืฉื™ืžื” ื”ืžืœืื” ื›ืืŸ) ื•ื”ื›ืชื•ื‘ืช ืฉืœ ื”ืฉืจืช ื”ืžืจื•ื—ืง ืฉืœื ื•.
ืจืฉื™ืžื” ืžืœืื” ืฉืœ ืคืจืžื˜ืจื™ื ืืคืฉืจื™ื™ื ื ื™ืชืŸ ืœืžืฆื•ื ื‘ืงื•ื‘ืฅ MLproject.

5. ื”ืขืจื›ืช ืชื•ืฆืื•ืช ืœืžื™ื“ื”

ืœืื—ืจ ื”ืฉืœืžืช ื”ื”ื“ืจื›ื”, ื ื•ื›ืœ ืœืขื‘ื•ืจ ื‘ื“ืคื“ืคืŸ ืœื›ืชื•ื‘ืช ื”ืฉืจืช ืฉืœื ื• http://server_host:server_port

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื›ืืŸ ืื ื• ืจื•ืื™ื ืจืฉื™ืžื” ืฉืœ ื›ืœ ื”ื ื™ืกื•ื™ื™ื (ืžืฉืžืืœ ืœืžืขืœื”), ื•ื›ืŸ ืžื™ื“ืข ืขืœ ืจื™ืฆื•ืช (ื‘ืืžืฆืข). ืื ื• ื™ื›ื•ืœื™ื ืœื”ืฆื™ื’ ืžื™ื“ืข ืžืคื•ืจื˜ ื™ื•ืชืจ (ืคืจืžื˜ืจื™ื, ืžื“ื“ื™ื, ื—ืคืฆื™ื ื•ืžื™ื“ืข ื ื•ืกืฃ) ืขื‘ื•ืจ ื›ืœ ื”ืฉืงื”.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ืขื‘ื•ืจ ื›ืœ ืžื“ื“ ื ื•ื›ืœ ืœืฆืคื•ืช ื‘ื”ื™ืกื˜ื•ืจื™ื” ืฉืœ ื”ืฉื™ื ื•ื™ื™ื

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื”ึธื”ึตืŸ. ื ื›ื•ืŸ ืœืขื›ืฉื™ื•, ืื ื• ื™ื›ื•ืœื™ื ืœื ืชื— ืืช ื”ืชื•ืฆืื•ืช ื‘ืžืฆื‘ "ื™ื“ื ื™", ื•ื ื™ืชืŸ ื’ื ืœื”ื’ื“ื™ืจ ืื™ืžื•ืช ืื•ื˜ื•ืžื˜ื™ ื‘ืืžืฆืขื•ืช ื”-API ืฉืœ MLflow.

6. ืจืฉื•ื ืืช ื”ื“ื’ื

ืœืื—ืจ ืฉื ื™ืชื—ื ื• ืืช ื”ื“ื’ื ืฉืœื ื• ื•ื”ื—ืœื˜ื ื• ืฉื”ื•ื ืžื•ื›ืŸ ืœืงืจื‘, ืื ื• ืžืžืฉื™ื›ื™ื ืœืจืฉื•ื ืื•ืชื•, ืœืฉื ื›ืš ื ื‘ื—ืจ ืืช ื”ื”ืฉืงื” ืฉืื ื• ืฆืจื™ื›ื™ื (ื›ืคื™ ืฉืžื•ืฆื’ ื‘ืคืกืงื” ื”ืงื•ื“ืžืช) ื•ื™ื•ืจื“ื™ื ืœืžื˜ื”.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ืœืื—ืจ ืฉืื ื• ื ื•ืชื ื™ื ืœื“ื’ื ืฉืœื ื• ืฉื, ื™ืฉ ืœื• ื’ืจืกื”. ืื ืชืฉืžืจื• ื“ื’ื ืื—ืจ ื‘ืื•ืชื• ืฉื, ื”ื’ืจืกื” ืชืฉื•ื“ืจื’ ืื•ื˜ื•ืžื˜ื™ืช.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ืขื‘ื•ืจ ื›ืœ ื“ื’ื, ื ื•ื›ืœ ืœื”ื•ืกื™ืฃ ืชื™ืื•ืจ ื•ืœื‘ื—ื•ืจ ื‘ืื—ื“ ืžืฉืœื•ืฉืช ื”ืžืฆื‘ื™ื (Staging, Production, Archived); ืœืื—ืจ ืžื›ืŸ, ื‘ืืžืฆืขื•ืช ื”-API, ื ื•ื›ืœ ืœื’ืฉืช ืœืžืฆื‘ื™ื ืืœื•, ืืฉืจ ื™ื—ื“ ืขื ื ื™ื”ื•ืœ ื’ืจืกืื•ืช ืžืกืคืงื™ื ื’ืžื™ืฉื•ืช ื ื•ืกืคืช.

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื™ืฉ ืœื ื• ื’ื ื’ื™ืฉื” ื ื•ื—ื” ืœื›ืœ ื”ื“ื’ืžื™ื

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื•ื”ื’ืจืกืื•ืช ืฉืœื”ื

MLOps - ืกืคืจ ื‘ื™ืฉื•ืœ, ืคืจืง 1

ื›ืžื• ื‘ืคืกืงื” ื”ืงื•ื“ืžืช, ื ื™ืชืŸ ืœื‘ืฆืข ืืช ื›ืœ ื”ืคืขื•ืœื•ืช ื‘ืืžืฆืขื•ืช ื”-API.

7. ืคืจื•ืก ืืช ื”ืžื•ื“ืœ

ื‘ืฉืœื‘ ื–ื”, ื™ืฉ ืœื ื• ื›ื‘ืจ ืžื•ื“ืœ (keras) ืžืื•ืžืŸ. ื“ื•ื’ืžื” ืœืื•ืคืŸ ืฉื‘ื• ืืชื” ื™ื›ื•ืœ ืœื”ืฉืชืžืฉ ื‘ื•:

class SegmentationModel:
    def __init__(self, tracking_uri, model_name):

        self.registry = RemoteRegistry(tracking_uri=tracking_uri)
        self.model_name = model_name
        self.model = self.build_model(model_name)

    def get_latest_model(self, model_name):
        registered_models = self.registry.get_registered_model(model_name)
        last_model = self.registry.get_last_model(registered_models)
        local_path = self.registry.download_artifact(last_model.run_id, 'model', './')
        return local_path

    def build_model(self, model_name):
        local_path = self.get_latest_model(model_name)

        return mlflow.keras.load_model(local_path)

    def predict(self, image):
        image = self.preprocess(image)
        result = self.model.predict(image)
        return self.postprocess(result)

    def preprocess(self, image):
        image = cv2.resize(image, (256, 256))
        image = image / 255.
        image = np.expand_dims(image, 0)
        return image

    def postprocess(self, result):
        return result

ื›ืืŸ self.registry ื”ื•ื ืฉื•ื‘ ืžืขื˜ืคืช ืงื˜ื ื” ืžืขืœ mlflow.tracking.MlflowClient, ืžื˜ืขืžื™ ื ื•ื—ื•ืช. ื”ื ืงื•ื“ื” ื”ื™ื ืฉืื ื™ ื ื™ื’ืฉ ืœืฉืจืช ืžืจื•ื—ืง ื•ืžื—ืคืฉ ืฉื ื“ื’ื ืขื ื”ืฉื ืฉืฆื•ื™ืŸ, ื•ื’ืจืกืช ื”ื™ื™ืฆื•ืจ ื”ืขื“ื›ื ื™ืช ื‘ื™ื•ืชืจ. ืœืื—ืจ ืžื›ืŸ, ืื ื™ ืžื•ืจื™ื“ ืืช ื”ื—ืคืฅ ื‘ืื•ืคืŸ ืžืงื•ืžื™ ืœืชื™ืงื™ื™ืช ./model ื•ื‘ื•ื ื” ืืช ื”ืžื•ื“ืœ ืžื”ืกืคืจื™ื™ื” ื”ื–ื• mlflow.keras.load_model(local_path). ืขื›ืฉื™ื• ืื ื—ื ื• ื™ื›ื•ืœื™ื ืœื”ืฉืชืžืฉ ื‘ืžื•ื“ืœ ืฉืœื ื•. ืžืคืชื—ื™ CV (ML) ื™ื›ื•ืœื™ื ื‘ืงืœื•ืช ืœืฉืคืจ ืืช ื”ืžื•ื“ืœ ื•ืœืคืจืกื ื’ืจืกืื•ืช ื—ื“ืฉื•ืช.

ืœืกื™ื›ื•ื

ื”ืฆื’ืชื™ ืžืขืจื›ืช ืฉืžืืคืฉืจืช:

  • ืื—ืกืŸ ืžื™ื“ืข ืžืจื›ื–ื™ ืขืœ ืžื•ื“ืœื™ื ืฉืœ ML, ื”ืชืงื“ืžื•ืช ื”ืื™ืžื•ืŸ ื•ื”ืชื•ืฆืื•ืช;
  • ืœืคืจื•ืก ื‘ืžื”ื™ืจื•ืช ืกื‘ื™ื‘ืช ืคื™ืชื•ื—;
  • ืœื ื˜ืจ ื•ืœื ืชื— ืืช ื”ืชืงื“ืžื•ืช ื”ืขื‘ื•ื“ื” ืขืœ ืžื•ื“ืœื™ื;
  • ื–ื” ื ื•ื— ืœื’ืจืกื” ื•ืœื ื”ืœ ืืช ืžืฆื‘ ื”ื“ื’ืžื™ื;
  • ืงืœ ืœืคืจื•ืก ืืช ื”ืžื•ื“ืœื™ื ื”ืžืชืงื‘ืœื™ื.

ื“ื•ื’ืžื” ื–ื• ื”ื™ื ืฆืขืฆื•ืข ื•ืžืฉืžืฉืช ื›ื ืงื•ื“ืช ื”ืชื—ืœื” ืœื‘ื ื™ื™ืช ืžืขืจื›ืช ืžืฉืœืš, ืฉืขืฉื•ื™ื” ืœื›ืœื•ืœ ืื•ื˜ื•ืžืฆื™ื” ืฉืœ ื”ืขืจื›ืช ืชื•ืฆืื•ืช ื•ืจื™ืฉื•ื ืžื•ื“ืœื™ื (ื ืงื•ื“ื•ืช 5 ื•-6, ื‘ื”ืชืืžื”) ืื• ืฉืชื•ืกื™ืฃ ื’ืจืกืื•ืช ืฉืœ ืžืขืจื›ื™ ื ืชื•ื ื™ื, ืื• ืื•ืœื™ ืžืฉื”ื• ืื—ืจ? ื”ื ืงื•ื“ื” ืฉื ื™ืกื™ืชื™ ืœื”ื‘ื”ื™ืจ ื”ื™ื ืฉืืชื” ืฆืจื™ืš MLOps ื‘ื›ืœืœื•ืชื•, MLflow ื”ื•ื ืจืง ืืžืฆืขื™ ืœื”ืฉื’ืช ืžื˜ืจื”.

ื›ืชื•ื‘ ื‘ืื™ืœื• ื‘ืขื™ื•ืช ื ืชืงืœืช ืฉืœื ื”ืฆื’ืชื™?
ืžื” ื”ื™ื™ืชื ืžื•ืกื™ืคื™ื ืœืžืขืจื›ืช ื›ื“ื™ ืฉืชืขื ื” ืขืœ ื”ืฆืจื›ื™ื ืฉืœื›ื?
ื‘ืื™ืœื• ื›ืœื™ื ื•ื’ื™ืฉื•ืช ืืชื” ืžืฉืชืžืฉ ื›ื“ื™ ืœืคืชื•ืจ ืืช ื›ืœ ื”ื‘ืขื™ื•ืช ืื• ื—ืœืงืŸ?

ื .ื‘. ืื ื™ ืืฉืื™ืจ ื›ืžื” ืงื™ืฉื•ืจื™ื:
ืคืจื•ื™ืงื˜ github - https://github.com/simbakot/mlflow_example
MLflow - https://mlflow.org/
ื“ื•ื"ืœ ื”ืขื‘ื•ื“ื” ืฉืœื™ ืœืฉืืœื•ืช - [ืžื•ื’ืŸ ื‘ื“ื•ื"ืœ]

ื”ื—ื‘ืจื” ืฉืœื ื• ืžืืจื—ืช ืžืขืช ืœืขืช ืื™ืจื•ืขื™ื ืฉื•ื ื™ื ืœืžื•ืžื—ื™ IT, ืœื“ื•ื’ืžื”: ื‘-8 ื‘ื™ื•ืœื™ ื‘ืฉืขื” 19:00 ืฉืขื•ืŸ ืžื•ืกืงื‘ื” ื™ืชืงื™ื™ื ืžืคื’ืฉ ืงื•ืจื•ืช ื—ื™ื™ื ื‘ืคื•ืจืžื˜ ืžืงื•ื•ืŸ, ืื ืืชื” ืžืขื•ื ื™ื™ืŸ, ืืชื” ื™ื›ื•ืœ ืœืงื—ืช ื—ืœืง, ื”ืจืฉืžื” ื›ืืŸ .

ืžืงื•ืจ: www.habr.com

ื”ื•ืกืคืช ืชื’ื•ื‘ื”