ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow

ื”ืขืœื, ื›ืึทื‘ืจืึธื•ื•ืกืง ืชื•ืฉื‘ื™ื. ื•ื•ื™ ืžื™ืจ ืฉื•ื™ืŸ ื’ืขืฉืจื™ื‘ืŸ, ื“ืขื ื—ื•ื“ืฉ OTUS ืื™ื– ืœืึธื ื˜ืฉื™ื ื’ ืฆื•ื•ื™ื™ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืงืึธืจืกืึทื– ืื™ืŸ ืึทืžืึธืœ, ื ื™ื™ืžืœื™ ื‘ืึทื–ืข ะธ ืึทื•ื•ืึทื ืกื™ืจื˜ืข. ืื™ืŸ ื“ืขื ืึทื›ื˜ื•ื ื’, ืžื™ืจ ืคืึธืจื–ืขืฆืŸ ืฆื• ื˜ื™ื™ืœืŸ ื ื•ืฆื™ืง ืžืึทื˜ืขืจื™ืึทืœ.

ื“ืขืจ ืฆื™ืœ ืคื•ืŸ ื“ืขื ืึทืจื˜ื™ืงืœ ืื™ื– ืฆื• ืจืขื“ืŸ ื•ื•ืขื’ืŸ ืื•ื ื“ื–ืขืจ ืขืจืฉื˜ืขืจ ื“ืขืจืคืึทืจื•ื ื’ ื ื™ืฆืŸ MLflow.

ืžื™ืจ ื•ื•ืขืœืŸ ืึธื ื”ื™ื™ื‘ืŸ ื“ื™ ืจืขืฆืขื ื–ื™ืข MLflow ืคึฟื•ืŸ ื–ื™ื™ืŸ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืื•ืŸ ืงืœืึธืฅ ืึทืœืข ื™ื˜ืขืจื™ื™ืฉืึทื ื– ืคื•ืŸ ื“ืขื ืœืขืจื ืขืŸ. ื“ืขืจื ืึธืš ืžื™ืจ ื•ื•ืขืœืŸ ื˜ื™ื™ืœืŸ ืื•ื ื“ื–ืขืจ ื“ืขืจืคืึทืจื•ื ื’ ืคื•ืŸ ืงืึทื ืขืงื˜ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow ื ื™ืฆืŸ UDF.

ืงืึธื ื˜ืขืงืกื˜

ืžื™ืจ ื–ืขื ืขืŸ ืื™ืŸ ืึทืœืฃ ื’ืขื–ื•ื ื˜ ืžื™ืจ ื ื•ืฆืŸ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืื•ืŸ ืงื™ื ืกื˜ืœืขืš ืกื™ื™ื›ืœ ืฆื• ื™ืžืคึผืึทื•ืขืจ ืžืขื ื˜ืฉืŸ ืฆื• ื ืขืžืขืŸ ื‘ืึทืฉื•ืœื“ื™ืงื•ื ื’ ืคื•ืŸ ื–ื™ื™ืขืจ ื’ืขื–ื•ื ื˜ ืื•ืŸ ื•ื•ื•ื™ืœื–ื™ื™ึทืŸ. ืึทื– ืก ื•ื•ืึธืก ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืžืึธื“ืขืœืก ื–ืขื ืขืŸ ื“ื™ ื”ืึทืจืฅ ืคื•ืŸ ื“ื™ ื“ืึทื˜ืŸ ื•ื•ื™ืกื ืฉืึทืคึฟื˜ ืคึผืจืึธื“ื•ืงื˜ืŸ ื•ื•ืึธืก ืžื™ืจ ืึทื ื˜ื•ื•ื™ืงืœืขืŸ, ืื•ืŸ ืึทื– ืก ื•ื•ืึธืก ืžื™ืจ ื–ืขื ืขืŸ ื’ืขืฆื•ื™ื’ืŸ ืฆื• MLflow, ืึทืŸ ืึธืคึฟืŸ ืžืงื•ืจ ืคึผืœืึทื˜ืคืึธืจืžืข ื•ื•ืึธืก ืงืึธื•ื•ืขืจืก ืึทืœืข ืึทืกืคึผืขืงืฅ ืคื•ืŸ ื“ื™ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืœื™ื™ืคืกื™ื™ืง.

MLflow

ื“ืขืจ ื”ื•ื™ืคึผื˜ ืฆื™ืœ ืคื•ืŸ MLflow ืื™ื– ืฆื• ืฆื•ืฉื˜ืขืœืŸ ืึทืŸ ื ืึธืš ืฉื™ื›ื˜ืข ืื•ื™ืฃ ืฉืคึผื™ืฅ ืคื•ืŸ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ื•ื•ืึธืก ื•ื•ืึธืœื˜ ืœืึธื–ืŸ ื“ืึทื˜ืŸ ืกื™ื™ืึทื ื˜ื™ืก ืฆื• ืึทืจื‘ืขื˜ืŸ ืžื™ื˜ ื›ึผืžืขื˜ ืงื™ื™ืŸ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ื‘ื™ื‘ืœื™ืึธื˜ืขืง (ื”ืงืกื ื•ืžืงืกืึธ, ืงืขืจืึทืก, ืžืœื™ืึทืคึผ, ืคึผื™ื˜ืึธืจื˜ืฉ, ืกืงืœืขืจืŸ ะธ tensorflow), ื’ืขื ื•ืžืขืŸ ืื™ืจ ืึทืจื‘ืขื˜ ืฆื• ื“ืขืจ ื•ื•ื™ื™ึทื˜ืขืจ ืžื“ืจื’ื”.

MLflow ื’ื™ื˜ ื“ืจื™ื™ ืงืึทืžืคึผืึธื•ื ืึทื ืฅ:

  • ื˜ืจืึทืงื™ื ื’ - ืจืขืงืึธืจื“ื™ื ื’ ืื•ืŸ ืจื™ืงื•ื•ืขืก ืคึฟืึทืจ ื™ืงืกืคึผืขืจืึทืžืึทื ืฅ: ืงืึธื“, ื“ืึทื˜ืŸ, ืงืึทื ืคื™ื’ื™ืขืจื™ื™ืฉืึทืŸ ืื•ืŸ ืจืขื–ื•ืœื˜ืึทื˜ืŸ. ืžืึธื ื™ื˜ืึธืจื™ื ื’ ื“ืขื ืคึผืจืึธืฆืขืก ืคื•ืŸ ืงืจื™ื™ื™ื˜ื™ื ื’ ืึท ืžืึธื“ืขืœ ืื™ื– ื–ื™ื™ืขืจ ื•ื•ื™ื›ื˜ื™ืง.
  • ืคึผืจืึทื“ื–ืฉืขืงืก - ืคึผืึทืงืงืึทื’ื™ื ื’ ืคึฟืึธืจืžืึทื˜ ืฆื• ืœื•ื™ืคืŸ ืื•ื™ืฃ ืงื™ื™ืŸ ืคึผืœืึทื˜ืคืึธืจืžืข (ืœืžืฉืœ. ืกืึทื’ืขืžืึทืงืขืจ)
  • ืžืึธื“ืขืœืก - ืึท ืคึผืจืึธืกื˜ ืคึฟืึธืจืžืึทื˜ ืคึฟืึทืจ ืกืึทื‘ืžื™ื˜ื™ื ื’ ืžืึธื“ืขืœืก ืฆื• ืคืึทืจืฉื™ื“ืŸ ื“ื™ืคึผืœื•ื™ืžืึทื ื˜ ืžื›ืฉื™ืจื™ื.

MLflow (ืื™ืŸ ืึทืœืฃ ืื™ืŸ ื“ืขืจ ืฆื™ื™ื˜ ืคื•ืŸ ืฉืจื™ื™ื‘ืŸ) ืื™ื– ืึทืŸ ืึธืคึฟืŸ ืžืงื•ืจ ืคึผืœืึทื˜ืคืึธืจืžืข ื•ื•ืึธืก ืึทืœืึทื•ื– ืื™ืจ ืฆื• ืคื™ืจืŸ ื“ื™ ืœื™ื™ืคืกื™ื™ืง ืคื•ืŸ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ, ืึทืจื™ื™ึทื ื’ืขืจืขื›ื ื˜ ื™ืงืกืคึผืขืจืึทืžืึทื ื˜ื™ื™ืฉืึทืŸ, ืจื™ื™ื•ืก ืื•ืŸ ื“ื™ืคึผืœื•ื™ืžืึทื ื˜.

ื‘ืึทืฉื˜ืขื˜ื™ืงืŸ MLflow

ืฆื• ื ื•ืฆืŸ MLflow ืื™ืจ ื“ืึทืจืคึฟืŸ ืฆื• ืขืจืฉื˜ืขืจ ืฉื˜ืขืœืŸ ื“ื™ื™ืŸ ื’ืึทื ืฅ ืคึผื™ื˜ื”ืึธืŸ ืกื•ื•ื™ื•ื•ืข, ืคึฟืึทืจ ื“ืขื ืžื™ืจ ื•ื•ืขืœืŸ ื ื•ืฆืŸ PyEnv (ืฆื• ื™ื ืกื˜ืึทืœื™ืจืŸ Python ืื•ื™ืฃ ืžืขืง, ื˜ืฉืขืง ืื•ื™ืก ื“ืึธ). ื“ืขื ื•ื•ืขื’ ืžื™ืจ ืงืขื ืขืŸ ืžืึทื›ืŸ ืึท ื•ื•ื™ืจื˜ื•ืึทืœ ืกื•ื•ื™ื•ื•ืข ื•ื•ื• ืžื™ืจ ื•ื•ืขืœืŸ ื™ื ืกื˜ืึทืœื™ืจืŸ ืึทืœืข ื“ื™ ืœื™ื™ื‘ืจืขืจื™ื– ื ื™ื™ื˜ื™ืง ืฆื• ืœื•ื™ืคืŸ ืขืก.

```
pyenv install 3.7.0
pyenv global 3.7.0 # Use Python 3.7
mkvirtualenv mlflow # Create a Virtual Env with Python 3.7
workon mlflow
```

ืœืึธืžื™ืจ ื™ื ืกื˜ืึทืœื™ืจืŸ ื“ื™ ืคืืจืœืื ื’ื˜ ืœื™ื™ื‘ืจืขืจื™ื–.

```
pip install mlflow==0.7.0 
            Cython==0.29  
            numpy==1.14.5 
            pandas==0.23.4 
            pyarrow==0.11.0
```

ื‘ืึทืžืขืจืงื•ื ื’: ืžื™ืจ ื ื•ืฆืŸ PyArrow ืฆื• ืœื•ื™ืคืŸ ืžืึธื“ืขืœืก ืึทื–ืึท ื•ื•ื™ UDF. ื“ื™ ื•ื•ืขืจืกื™ืขืก ืคื•ืŸ PyArrow ืื•ืŸ Numpy ื“ืืจืฃ ื–ื™ื™ืŸ ืคืึทืจืคืขืกื˜ื™ืงื˜ ื•ื•ื™ื™ึทืœ ื“ื™ ืœืขืฆื˜ืข ื•ื•ืขืจืกื™ืขืก ื”ืึธื‘ืŸ ืงืึธื ืคืœื™ืงื˜ ืžื™ื˜ ื™ืขื“ืขืจ ืื ื“ืขืจืข.

ืงืึทื˜ืขืจ ื˜ืจืึทืงื™ื ื’ ื•ื™

MLflow ื˜ืจืึทืงื™ื ื’ ืึทืœืึทื•ื– ืื•ื ื“ื– ืฆื• ืงืœืึธืฅ ืื•ืŸ ืึธื ืคึฟืจืขื’ ื™ืงืกืคึผืขืจืึทืžืึทื ืฅ ื ื™ืฆืŸ Python ืื•ืŸ ืจื• API. ืื™ืŸ ืึทื“ื™ืฉืึทืŸ, ืื™ืจ ืงืขื ืขืŸ ื‘ืึทืฉืœื™ืกืŸ ื•ื•ื• ืฆื• ืงืจืึธื ืžืึธื“ืขืœ ืึทืจื˜ืึทืคืึทืงืฅ (localhost, ืึทืžืึทื–ืึธืŸ ืกืงืกื ื•ืžืงืก, Azure ื‘ืœืึธื‘ ืกื˜ืึธืจื™ื“ื–ืฉ, Google ืงืœืึธื•ื“ ืกื˜ืึธืจื™ื“ื–ืฉ ืึธื“ืขืจ SFTP ืกืขืจื•ื•ืขืจ). ื–ื™ื ื˜ ืžื™ืจ ื ื•ืฆืŸ AWS ื‘ื™ื™ Alpha Health, ืื•ื ื“ื–ืขืจ ืึทืจื˜ืึทืคืึทืงื˜ ืกื˜ืึธืจื™ื“ื–ืฉ ื•ื•ืขื˜ ื–ื™ื™ืŸ S3.

# Running a Tracking Server
mlflow server 
    --file-store /tmp/mlflow/fileStore 
    --default-artifact-root s3://<bucket>/mlflow/artifacts/ 
    --host localhost
    --port 5000

MLflow ืจืขืงืึทืžืขื ื“ื– ื ื™ืฆืŸ ืคึผืขืจืกื™ืกื˜ืขื ื˜ ื˜ืขืงืข ืกื˜ืึธืจื™ื“ื–ืฉ. ื˜ืขืงืข ืกื˜ืึธืจื™ื“ื–ืฉ ืื™ื– ื•ื•ื• ื“ืขืจ ืกืขืจื•ื•ืขืจ ื•ื•ืขื˜ ืงืจืึธื ืœื•ื™ืคืŸ ืื•ืŸ ืขืงืกืคึผืขืจื™ืžืขื ื˜ ืžืขื˜ืึทื“ืึทื˜ืึท. ื•ื•ืขืŸ ืื™ืจ ืึธื ื”ื™ื™ื‘ืŸ ื“ืขื ืกืขืจื•ื•ืขืจ, ืžืึทื›ืŸ ื–ื™ื›ืขืจ ืึทื– ืขืก ื•ื•ื™ื™ื–ื˜ ืฆื• ื“ื™ ืคึผืขืจืกื™ืกื˜ืขื ื˜ ื˜ืขืงืข ืงืจืึธื. ื“ืึธ ืคึฟืึทืจ ื“ืขืจ ืขืงืกืคึผืขืจื™ืžืขื ื˜ ืžื™ืจ ื•ื•ืขืœืŸ ืคืฉื•ื˜ ื ื•ืฆืŸ /tmp.

ื’ืขื“ืขื ืงื˜ ืึทื– ืื•ื™ื‘ ืžื™ืจ ื•ื•ื™ืœืŸ ืฆื• ื ื•ืฆืŸ ื“ื™ mlflow ืกืขืจื•ื•ืขืจ ืฆื• ืœื•ื™ืคืŸ ืึทืœื˜ ื™ืงืกืคึผืขืจืึทืžืึทื ืฅ, ื–ื™ื™ ืžื•ื–ืŸ ื–ื™ื™ืŸ ืคืึธืจืฉื˜ืขืœืŸ ืื™ืŸ ื“ืขืจ ื˜ืขืงืข ืกื˜ืึธืจื™ื“ื–ืฉ. ืึธื‘ืขืจ, ืืคื™ืœื• ืึธืŸ ื“ืขื, ืžื™ืจ ืงืขืŸ ื ื•ืฆืŸ ื–ื™ื™ ืื™ืŸ ื“ื™ UDF, ื•ื•ื™ื™ึทืœ ืžื™ืจ ื ืึธืจ ื“ืึทืจืคึฟืŸ ื“ื™ ื•ื•ืขื’ ืฆื• ื“ื™ ืžืึธื“ืขืœ.

ื‘ืึทืžืขืจืงื•ื ื’: ื”ืึทืœื˜ืŸ ืื™ืŸ ืžื™ื™ื ื•ื ื’ ืึทื– ื˜ืจืึทืงื™ื ื’ ื•ื™ ืื•ืŸ ื“ืขืจ ืžืึธื“ืขืœ ืงืœื™ืขื ื˜ ืžื•ื–ืŸ ื”ืึธื‘ืŸ ืึทืงืกืขืก ืฆื• ื“ื™ ืึทืจื˜ืึทืคืึทืงื˜ ืึธืจื˜. ื“ืึธืก ืื™ื–, ืจืึทื’ืึทืจื“ืœืึทืก ืคื•ืŸ ื“ื™ ืคืึทืงื˜ ืึทื– ื“ื™ ื˜ืจืึทืงื™ื ื’ ื•ื™ ืจื™ื–ื™ื™ื“ื– ืื™ืŸ ืึทืŸ EC2 ื‘ื™ื™ึทืฉืคึผื™ืœ, ื•ื•ืขืŸ ืคืœื™ืกื ื“ื™ืง MLflow ืœืึธื•ืงืึทืœื™, ื“ื™ ืžืึทืฉื™ืŸ ืžื•ื–ืŸ ื”ืึธื‘ืŸ ื“ื™ืจืขืงื˜ ืึทืงืกืขืก ืฆื• S3 ืฆื• ืฉืจื™ื™ึทื‘ืŸ ืึทืจื˜ืึทืคืึทืงื˜ ืžืึธื“ืขืœืก.

ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow
ื˜ืจืึทืงื™ื ื’ ื•ื™ ืกื˜ืึธืจื– ืึทืจื˜ืึทืคืึทืงืฅ ืื™ืŸ ืึท S3 ืขืžืขืจ

ืคืœื™ืกื ื“ื™ืง ืžืึธื“ืขืœืก

ื•ื•ื™ ื‘ืึทืœื“ ื•ื•ื™ ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืื™ื– ืคืœื™ืกื ื“ื™ืง, ืื™ืจ ืงืขื ืขืŸ ืึธื ื”ื™ื™ื‘ืŸ ื˜ืจื™ื™ื ื™ื ื’ ื“ื™ ืžืึธื“ืขืœืก.

ื•ื•ื™ ืึท ื‘ื™ื™ืฉืคึผื™ืœ, ืžื™ืจ ื•ื•ืขืœืŸ ื ื•ืฆืŸ ื“ื™ ื•ื•ื™ื™ึทืŸ ืžืึธื“ื™ืคื™ืงืึทื˜ื™ืึธืŸ ืคื•ืŸ ื“ื™ MLflow ื‘ื™ื™ืฉืคึผื™ืœ ืื™ืŸ ืกืงืœืขืึทืจืŸ.

MLFLOW_TRACKING_URI=http://localhost:5000 python wine_quality.py 
  --alpha 0.9
  --l1_ration 0.5
  --wine_file ./data/winequality-red.csv

ื•ื•ื™ ืžื™ืจ ื”ืึธื‘ืŸ ืฉื•ื™ืŸ ื“ื™ืกืงืึทืกื˜, MLflow ืึทืœืึทื•ื– ืื™ืจ ืฆื• ืงืœืึธืฅ ืžืึธื“ืขืœ ืคึผืึทืจืึทืžืขื˜ืขืจืก, ืžืขื˜ืจื™ืงืก ืื•ืŸ ืึทืจื˜ืึทืคืึทืงืฅ ืึทื–ื•ื™ ืื™ืจ ืงืขื ืขืŸ ืฉืคึผื•ืจ ื•ื•ื™ ื–ื™ื™ ื™ื•ื•ืึทืœื•ื• ืื™ื‘ืขืจ ื™ื˜ืขืจื™ื™ืฉืึทื ื–. ื“ืขืจ ืฉื˜ืจื™ืš ืื™ื– ื’ืึธืจ ื ื•ืฆื™ืง ื•ื•ื™ื™ึทืœ ืึทื–ื•ื™ ืžื™ืจ ืงืขื ืขืŸ ืจืขืคึผืจืึธื“ื•ืฆื™ืจืŸ ื“ืขืจ ื‘ืขืกื˜ืขืจ ืžืึธื“ืขืœ ื“ื•ืจืš ืงืึธื ื˜ืึทืงื˜ ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืึธื“ืขืจ ืคึฟืึทืจืฉื˜ื™ื™ืŸ ื•ื•ืึธืก ืงืึธื“ ื“ื•ืจื›ื’ืขืงืึธื›ื˜ ื“ื™ ืคืืจืœืื ื’ื˜ ื™ื˜ืขืจืึทื˜ื™ืึธืŸ ืžื™ื˜ ื“ื™ ื’ื™ื˜ ื”ืึทืฉ ืœืึธื’ืก ืคื•ืŸ ืงืึทืžื™ืฅ.

with mlflow.start_run():

    ... model ...

    mlflow.log_param("source", wine_path)
    mlflow.log_param("alpha", alpha)
    mlflow.log_param("l1_ratio", l1_ratio)

    mlflow.log_metric("rmse", rmse)
    mlflow.log_metric("r2", r2)
    mlflow.log_metric("mae", mae)

    mlflow.set_tag('domain', 'wine')
    mlflow.set_tag('predict', 'quality')
    mlflow.sklearn.log_model(lr, "model")

ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow
ื•ื•ื™ื™ึทืŸ ื™ื˜ืขืจื™ื™ืฉืึทื ื–

ืกืขืจื•ื•ื™ืจืขืจ ื˜ื™ื™ืœ ืคึฟืึทืจ ื“ื™ ืžืึธื“ืขืœ

ื“ื™ MLflow ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ, ืœืึธื ื˜ืฉื˜ ืžื™ื˜ ื“ื™ "mlflow ืกืขืจื•ื•ืขืจ" ื‘ืึทืคึฟืขืœ, ื”ืื˜ ืึท REST API ืคึฟืึทืจ ื˜ืจืึทืงื™ื ื’ ืจืึทื ื– ืื•ืŸ ืฉืจื™ื™ื‘ืŸ ื“ืึทื˜ืŸ ืฆื• ื“ื™ ื”ื™ื’ืข ื˜ืขืงืข ืกื™ืกื˜ืขื. ืื™ืจ ืงืขื ื˜ ืกืคึผืขืฆื™ืคื™ืฆื™ืจืŸ ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืึทื“ืจืขืก ื ื™ืฆืŸ ื“ื™ ืกื•ื•ื™ื•ื•ืข ื•ื•ืขืจื™ืึทื‘ืึทืœื– "MLFLOW_TRACKING_URI" ืื•ืŸ ื“ื™ MLflow ื˜ืจืึทืงื™ื ื’ ืึทืคึผื™ ื•ื•ืขื˜ ืื•ื™ื˜ืึธืžืึทื˜ื™ืฉ ืงืึธื ื˜ืึทืงื˜ ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืื•ื™ืฃ ื“ืขื ืึทื“ืจืขืก ืฆื• ืฉืึทืคึฟืŸ / ื‘ืึทืงื•ืžืขืŸ ืงืึทื˜ืขืจ ืื™ื ืคึฟืึธืจืžืึทืฆื™ืข, ืงืœืึธืฅ ืžืขื˜ืจื™ืงืก, ืขื˜ืง.

ืžืงื•ืจ: ื“ืึธืงืก // ืคืœื™ืกื ื“ื™ืง ืึท ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ

ืฆื• ืฆื•ืฉื˜ืขืœืŸ ื“ื™ ืžืึธื“ืขืœ ืžื™ื˜ ืึท ืกืขืจื•ื•ืขืจ, ืžื™ืจ ื“ืึทืจืคึฟืŸ ืึท ืคืœื™ืกื ื“ื™ืง ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ (ื–ืขืŸ ืงืึทื˜ืขืจ ืฆื•ื‘ื™ื ื“) ืื•ืŸ ื“ื™ Run ID ืคื•ืŸ ื“ื™ ืžืึธื“ืขืœ.

ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow
ืœื•ื™ืคืŸ ID

# Serve a sklearn model through 127.0.0.0:5005
MLFLOW_TRACKING_URI=http://0.0.0.0:5000 mlflow sklearn serve 
  --port 5005  
  --run_id 0f8691808e914d1087cf097a08730f17 
  --model-path model

ืฆื• ื“ื™ื ืขืŸ ืžืึธื“ืขืœืก ื ื™ืฆืŸ ื“ื™ MLflow ื“ื™ื ืขืŸ ืคืึทื ื’ืงืฉืึทื ืึทืœื™ื˜ื™, ืžื™ืจ ื“ืึทืจืคึฟืŸ ืึทืงืกืขืก ืฆื• ื“ื™ ื˜ืจืึทืงื™ื ื’ ื•ื™ ืฆื• ื‘ืึทืงื•ืžืขืŸ ืื™ื ืคึฟืึธืจืžืึทืฆื™ืข ื•ื•ืขื’ืŸ ื“ืขื ืžืึธื“ืขืœ ืคืฉื•ื˜ ื“ื•ืจืš ืกืคึผืขืฆื™ืคื™ืฆื™ืจืŸ --run_id.

ืึทืžืึธืœ ื“ืขืจ ืžืึธื“ืขืœ ืงืึธื ื˜ืึทืงื˜ ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ, ืžื™ืจ ืงืขื ืขืŸ ื‘ืึทืงื•ืžืขืŸ ืึท ื ื™ื™ึทืข ืžืึธื“ืขืœ ืขื ื“ืคึผื•ื™ื ื˜.

# Query Tracking Server Endpoint
curl -X POST 
  http://127.0.0.1:5005/invocations 
  -H 'Content-Type: application/json' 
  -d '[
	{
		"fixed acidity": 3.42, 
		"volatile acidity": 1.66, 
		"citric acid": 0.48, 
		"residual sugar": 4.2, 
		"chloridessssss": 0.229, 
		"free sulfur dsioxide": 19, 
		"total sulfur dioxide": 25, 
		"density": 1.98, 
		"pH": 5.33, 
		"sulphates": 4.39, 
		"alcohol": 10.8
	}
]'

> {"predictions": [5.825055635303461]}

ืคืœื™ืกื ื“ื™ืง ืžืึธื“ืขืœืก ืคื•ืŸ ืกืคึผืึทืจืง

ื˜ืจืึธืฅ ื“ืขืจ ืคืึทืงื˜ ืึทื– ื“ื™ ื˜ืจืึทืงื™ื ื’ ืกืขืจื•ื•ืขืจ ืื™ื– ืฉื˜ืึทืจืง ื’ืขื ื•ื’ ืฆื• ื“ื™ื ืขืŸ ืžืึธื“ืขืœืก ืื™ืŸ ืคืึทืงื˜ื™ืฉ ืฆื™ื™ื˜, ื‘ืึทืŸ ื–ื™ื™ ืื•ืŸ ื ื•ืฆืŸ ื“ื™ ืกืขืจื•ื•ืขืจ ืคืึทื ื’ืงืฉืึทื ืึทืœื™ื˜ื™ (ืžืงื•ืจ: mlflow // ื“ืึธืงืก // ืžืึธื“ืขืœืก # ื”ื™ื’ืข), ื ื™ืฆืŸ Spark (ืคึผืขืงืœ ืึธื“ืขืจ ืกื˜ืจื™ืžื™ื ื’) ืื™ื– ืึทืŸ ืืคื™ืœื• ืžืขืจ ืฉื˜ืึทืจืง ืœื™ื™ื–ื•ื ื’ ืจืขื›ื˜ ืฆื• ื“ืขืจ ืคืึทืจืฉืคึผืจื™ื™ื˜ื•ื ื’.

ื™ืžืึทื“ื–ืฉืึทืŸ ืึทื– ืื™ืจ ืคืฉื•ื˜ ื“ื•ืจื›ื’ืขืงืึธื›ื˜ ื“ื™ ื˜ืจื™ื™ื ื™ื ื’ ืึธืคืคืœื™ื ืข ืื•ืŸ ื“ืขืžืึธืœื˜ ื’ืขื•ื•ืขื ื“ื˜ ื“ื™ ืคึผืจืึธื“ื•ืงืฆื™ืข ืžืึธื“ืขืœ ืฆื• ืึทืœืข ื“ื™ื™ืŸ ื“ืึทื˜ืŸ. ื“ืึธืก ืื™ื– ื•ื•ื• Spark ืื•ืŸ MLflow ืฉื™ื™ึทื ืขืŸ.

ื™ื ืกื˜ืึทืœื™ืจืŸ PySpark + Jupyter + Spark

ืžืงื•ืจ: ื‘ืึทืงื•ืžืขืŸ ืกื˜ืึทืจื˜ืขื“ PySpark - Jupyter

ืฆื• ื•ื•ื™ื™ึทื–ืŸ ื•ื•ื™ ืžื™ืจ ืฆื•ืœื™ื™ื’ืŸ MLflow ืžืึธื“ืขืœืก ืฆื• ืกืคึผืึทืจืง ื“ืึทื˜ืึทืคืจืึทืžืขืก, ืžื™ืจ ื“ืึทืจืคึฟืŸ ืฆื• ืฉื˜ืขืœืŸ ื“ื–ืฉื•ืคึผื™ื˜ืขืจ ื ืึธื•ื˜ื‘ื•ืงืก ืฆื• ืึทืจื‘ืขื˜ืŸ ืฆื•ื–ืึทืžืขืŸ ืžื™ื˜ PySpark.

ืึธื ื”ื™ื™ื‘ ื“ื•ืจืš ื™ื ืกื˜ืึธืœื™ื ื’ ื“ื™ ืœืขืฆื˜ืข ืกื˜ืึทื‘ื™ืœ ื•ื•ืขืจืกื™ืข ืึทืคึผืึทื˜ืฉื™ ืกืคึผืึทืจืง:

cd ~/Downloads/
tar -xzf spark-2.4.3-bin-hadoop2.7.tgz
mv ~/Downloads/spark-2.4.3-bin-hadoop2.7 ~/
ln -s ~/spark-2.4.3-bin-hadoop2.7 ~/sparkฬ€

ื™ื ืกื˜ืึทืœื™ืจืŸ PySpark ืื•ืŸ Jupyter ืื™ืŸ ื“ื™ ื•ื•ื™ืจื˜ื•ืึทืœ ืกื•ื•ื™ื•ื•ืข:

pip install pyspark jupyter

ื‘ืึทืฉื˜ืขื˜ื™ืงืŸ ืกื•ื•ื™ื•ื•ืข ื•ื•ืขืจื™ืึทื‘ืึทืœื–:

export SPARK_HOME=~/spark
export PATH=$SPARK_HOME/bin:$PATH
export PYSPARK_DRIVER_PYTHON=jupyter
export PYSPARK_DRIVER_PYTHON_OPTS="notebook --notebook-dir=${HOME}/Projects/notebooks"

ื•ื•ื™ื™ืœ ื‘ืืฉืœืืกืŸ notebook-dir, ืžื™ืจ ืงืขื ืขืŸ ืงืจืึธื ืื•ื ื“ื–ืขืจ ื ืึธื•ื˜ื‘ื•ืงืก ืื™ืŸ ื“ืขืจ ื’ืขื•ื•ืืœื˜ ื˜ืขืงืข.

ืœืึธื ื˜ืฉื™ื ื’ Jupyter ืคึฟื•ืŸ PySpark

ื–ื™ื ื˜ ืžื™ืจ ื–ืขื ืขืŸ ื‘ื™ื›ื•ืœืช ืฆื• ืงืึทื ืคื™ื’ื™ืขืจ ื“ื–ืฉื•ืคึผื™ื˜ืขืจ ื•ื•ื™ ืึท PySpark ืฉืึธืคืขืจ, ืžื™ืจ ืงืขื ืขืŸ ืื™ืฆื˜ ืœื•ื™ืคืŸ Jupyter ื”ืขืคื˜ ืื™ืŸ ื“ืขื ืงืึธื ื˜ืขืงืกื˜ ืคื•ืŸ PySpark.

(mlflow) afranzi:~$ pyspark
[I 19:05:01.572 NotebookApp] sparkmagic extension enabled!
[I 19:05:01.573 NotebookApp] Serving notebooks from local directory: /Users/afranzi/Projects/notebooks
[I 19:05:01.573 NotebookApp] The Jupyter Notebook is running at:
[I 19:05:01.573 NotebookApp] http://localhost:8888/?token=c06252daa6a12cfdd33c1d2e96c8d3b19d90e9f6fc171745
[I 19:05:01.573 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 19:05:01.574 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=c06252daa6a12cfdd33c1d2e96c8d3b19d90e9f6fc171745

ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow

ื•ื•ื™ ื“ืขืจืžืื ื˜ ืื•ื™ื‘ืŸ, MLflow ื’ื™ื˜ ืึท ืฉื˜ืจื™ืš ืคึฟืึทืจ ืœืึธื’ื™ื ื’ ืžืึธื“ืขืœ ืึทืจื˜ืึทืคืึทืงืฅ ืื™ืŸ S3. ื•ื•ื™ ื‘ืึทืœื“ ื•ื•ื™ ืžื™ืจ ื”ืึธื‘ืŸ ื“ื™ ืื•ื™ืกื’ืขืงืœื™ื‘ืŸ ืžืึธื“ืขืœ ืื™ืŸ ืื•ื ื“ื–ืขืจ ื”ืขื ื˜, ืžื™ืจ ื”ืึธื‘ืŸ ื“ื™ ื’ืขืœืขื’ื ื”ื™ื™ื˜ ืฆื• ืึทืจื™ื™ึทื ืคื™ืจ ืขืก ื•ื•ื™ ืึท UDF ื ื™ืฆืŸ ื“ื™ ืžืึธื“ื•ืœืข mlflow.pyfunc.

import mlflow.pyfunc

model_path = 's3://<bucket>/mlflow/artifacts/1/0f8691808e914d1087cf097a08730f17/artifacts/model'
wine_path = '/Users/afranzi/Projects/data/winequality-red.csv'
wine_udf = mlflow.pyfunc.spark_udf(spark, model_path)

df = spark.read.format("csv").option("header", "true").option('delimiter', ';').load(wine_path)
columns = [ "fixed acidity", "volatile acidity", "citric acid",
            "residual sugar", "chlorides", "free sulfur dioxide",
            "total sulfur dioxide", "density", "pH",
            "sulphates", "alcohol"
          ]
          
df.withColumn('prediction', wine_udf(*columns)).show(100, False)

ื™ืงืกื˜ืขื ื“ื™ื ื’ ืกืคึผืึทืจืง ืžื™ื˜ MLflow
PySpark - ืึทืจื•ื™ืกืคื™ืจืŸ ื•ื•ื™ื™ึทืŸ ืงื•ื•ืึทืœื™ื˜ืขื˜ ืคึฟืึธืจื•ื™ืกื–ืึธื’ืŸ

ื‘ื™ื– ืฆื• ื“ืขื ืคื•ื ื˜, ืžื™ืจ ื”ืึธื‘ืŸ ื’ืขืจืขื“ื˜ ื•ื•ืขื’ืŸ ื•ื•ื™ ืฆื• ื ื•ืฆืŸ PySpark ืžื™ื˜ MLflow, ืžื™ื˜ ื•ื•ื™ื™ึทืŸ ืงื•ื•ืึทืœื™ื˜ืขื˜ ืคึฟืึธืจื•ื™ืกื–ืึธื’ืŸ ืื•ื™ืฃ ื“ื™ ื’ืื ืฆืข ื•ื•ื™ื™ึทืŸ ื“ืึทื˜ืึทืกืขื˜. ืึธื‘ืขืจ ื•ื•ืึธืก ืื•ื™ื‘ ืื™ืจ ื“ืึทืจืคึฟืŸ ืฆื• ื ื•ืฆืŸ Python MLflow ืžืึทื“ื–ืฉื•ืœื– ืคึฟื•ืŸ Scala Spark?

ืžื™ืจ ื˜ืขืกื˜ืขื“ ื“ืึธืก ืื•ื™ืš ื“ื•ืจืš ืกืคึผืœื™ื˜ื™ื ื’ ื“ื™ ืกืคึผืึทืจืง ืงืึธื ื˜ืขืงืกื˜ ืฆื•ื•ื™ืฉืŸ ืกืงืึทืœืึท ืื•ืŸ ืคึผื™ื˜ื”ืึธืŸ. ื“ืึธืก ืื™ื–, ืžื™ืจ ืจืขื’ื™ืกื˜ืจื™ืจื˜ MLflow UDF ืื™ืŸ Python ืื•ืŸ ื’ืขื•ื•ื™ื™ื ื˜ ืขืก ืคึฟื•ืŸ ืกืงืึทืœืึท (ื™ืึธ, ื˜ืึธืžืขืจ ื ื™ืฉื˜ ื“ืขืจ ื‘ืขืกื˜ืขืจ ืœื™ื™ื–ื•ื ื’, ืึธื‘ืขืจ ื•ื•ืึธืก ืžื™ืจ ื”ืึธื‘ืŸ).

ืกืงืึทืœืึท ืกืคึผืึทืจืง + ืžืœืคืœืึธื•ื•

ืคึฟืึทืจ ื“ืขื ื‘ื™ื™ึทืฉืคึผื™ืœ ืžื™ืจ ื•ื•ืขืœืŸ ืœื™ื™ื’ืŸ ื˜ื•ืจืข ืงืขืจื ืขืœ ืื™ืŸ ื“ื™ ื™ื’ื–ื™ืกื˜ื™ื ื’ ื“ื–ืฉื•ืคึผื™ื˜ืขืจ.

ื™ื ืกื˜ืึทืœื™ืจืŸ Spark + Toree + Jupyter

pip install toree
jupyter toree install --spark_home=${SPARK_HOME} --sys-prefix
jupyter kernelspec list
```
```
Available kernels:
  apache_toree_scala    /Users/afranzi/.virtualenvs/mlflow/share/jupyter/kernels/apache_toree_scala
  python3               /Users/afranzi/.virtualenvs/mlflow/share/jupyter/kernels/python3
```

ื•ื•ื™ ืื™ืจ ืงืขื ืขืŸ ื–ืขืŸ ืคึฟื•ืŸ ื“ื™ ืึทื˜ืึทื˜ืฉื˜ ื”ืขืคื˜, ื“ื™ UDF ืื™ื– ืฉืขืจื“ ืฆื•ื•ื™ืฉืŸ Spark ืื•ืŸ PySpark. ืžื™ืจ ื”ืึธืคืŸ ืึทื– ื“ืขืจ ื˜ื™ื™ืœ ื•ื•ืขื˜ ื–ื™ื™ืŸ ื ื•ืฆื™ืง ืคึฟืึทืจ ื™ืขื ืข ื•ื•ืืก ืœื™ื‘ ืกืงืึทืœืึท ืื•ืŸ ื•ื•ื™ืœืŸ ืฆื• ื ื•ืฆืŸ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืžืึธื“ืขืœืก ืื™ืŸ ืคึผืจืึธื“ื•ืงืฆื™ืข.

import org.apache.spark.sql.functions.col
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.{Column, DataFrame}
import scala.util.matching.Regex

val FirstAtRe: Regex = "^_".r
val AliasRe: Regex = "[\s_.:@]+".r

def getFieldAlias(field_name: String): String = {
    FirstAtRe.replaceAllIn(AliasRe.replaceAllIn(field_name, "_"), "")
}

def selectFieldsNormalized(columns: List[String])(df: DataFrame): DataFrame = {
    val fieldsToSelect: List[Column] = columns.map(field =>
        col(field).as(getFieldAlias(field))
    )
    df.select(fieldsToSelect: _*)
}

def normalizeSchema(df: DataFrame): DataFrame = {
    val schema = df.columns.toList
    df.transform(selectFieldsNormalized(schema))
}

FirstAtRe = ^_
AliasRe = [s_.:@]+

getFieldAlias: (field_name: String)String
selectFieldsNormalized: (columns: List[String])(df: org.apache.spark.sql.DataFrame)org.apache.spark.sql.DataFrame
normalizeSchema: (df: org.apache.spark.sql.DataFrame)org.apache.spark.sql.DataFrame
Out[1]:
[s_.:@]+
In [2]:
val winePath = "~/Research/mlflow-workshop/examples/wine_quality/data/winequality-red.csv"
val modelPath = "/tmp/mlflow/artifactStore/0/96cba14c6e4b452e937eb5072467bf79/artifacts/model"

winePath = ~/Research/mlflow-workshop/examples/wine_quality/data/winequality-red.csv
modelPath = /tmp/mlflow/artifactStore/0/96cba14c6e4b452e937eb5072467bf79/artifacts/model
Out[2]:
/tmp/mlflow/artifactStore/0/96cba14c6e4b452e937eb5072467bf79/artifacts/model
In [3]:
val df = spark.read
              .format("csv")
              .option("header", "true")
              .option("delimiter", ";")
              .load(winePath)
              .transform(normalizeSchema)

df = [fixed_acidity: string, volatile_acidity: string ... 10 more fields]
Out[3]:
[fixed_acidity: string, volatile_acidity: string ... 10 more fields]
In [4]:
%%PySpark
import mlflow
from mlflow import pyfunc

model_path = "/tmp/mlflow/artifactStore/0/96cba14c6e4b452e937eb5072467bf79/artifacts/model"
wine_quality_udf = mlflow.pyfunc.spark_udf(spark, model_path)

spark.udf.register("wineQuality", wine_quality_udf)
Out[4]:
<function spark_udf.<locals>.predict at 0x1116a98c8>
In [6]:
df.createOrReplaceTempView("wines")
In [10]:
%%SQL
SELECT 
    quality,
    wineQuality(
        fixed_acidity,
        volatile_acidity,
        citric_acid,
        residual_sugar,
        chlorides,
        free_sulfur_dioxide,
        total_sulfur_dioxide,
        density,
        pH,
        sulphates,
        alcohol
    ) AS prediction
FROM wines
LIMIT 10
Out[10]:
+-------+------------------+
|quality|        prediction|
+-------+------------------+
|      5| 5.576883967129615|
|      5|  5.50664776916154|
|      5| 5.525504822954496|
|      6| 5.504311247097457|
|      5| 5.576883967129615|
|      5|5.5556903912725755|
|      5| 5.467882654744997|
|      7| 5.710602976324739|
|      7| 5.657319539336507|
|      5| 5.345098606538708|
+-------+------------------+

In [17]:
spark.catalog.listFunctions.filter('name like "%wineQuality%").show(20, false)

+-----------+--------+-----------+---------+-----------+
|name       |database|description|className|isTemporary|
+-----------+--------+-----------+---------+-----------+
|wineQuality|null    |null       |null     |true       |
+-----------+--------+-----------+---------+-----------+

ื•ื•ื™ื™ึทื˜ืขืจ ืกื˜ืขืคึผืก

ืืคื™ืœื• MLflow ืื™ื– ืื™ืŸ ืึทืœืฃ ื•ื•ืขืจืกื™ืข ืื™ืŸ ื“ืขืจ ืฆื™ื™ื˜ ืคื•ืŸ ืฉืจื™ื™ื‘ืŸ, ืขืก ืงื•ืงื˜ ื’ืึทื ืฅ ืคึผืจืึทืžืึทืกื™ื ื’. ื ืึธืจ ื“ื™ ืคื™ื™ื™ืงื™ื™ื˜ ืฆื• ืœื•ื™ืคืŸ ืงื™ื™ืคืœ ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ ืคืจืึทืžืขื•ื•ืึธืจืงืก ืื•ืŸ ืคืึทืจื ื•ืฆืŸ ื–ื™ื™ ืคึฟื•ืŸ ืึท ืื™ื™ืŸ ืขื ื“ืคึผื•ื™ื ื˜ ื ืขืžื˜ ืจืขืงืึธืžืขื ื“ืึทื˜ืึธืจ ืกื™ืกื˜ืขืžืขืŸ ืฆื• ื“ืขืจ ื•ื•ื™ื™ึทื˜ืขืจ ืžื“ืจื’ื”.

ืื™ืŸ ืึทื“ื™ืฉืึทืŸ, MLflow ื‘ืจืขื ื’ื˜ ื“ืึทื˜ืึท ืขื ื“ื–ืฉืึทื ื™ืจื– ืื•ืŸ ื“ืึทื˜ืึท ื•ื•ื™ืกื ืฉืึทืคึฟื˜ ืกืคึผืขืฉืึทืœืึทืกืฅ ื ืขืขื ื˜ืขืจ ืฆื•ื–ืึทืžืขืŸ, ืืจื•ื™ืคืœื™ื™ื’ืŸ ืึท ืคึผืจืึธืกื˜ ืฉื™ื›ื˜ืข ืฆื•ื•ื™ืฉืŸ ื–ื™ื™.

ื ืึธืš ื“ืขื ื•ื™ืกืคืึธืจืฉื•ื ื’ ืคื•ืŸ MLflow, ืžื™ืจ ื–ืขื ืขืŸ ื–ื™ื›ืขืจ ืึทื– ืžื™ืจ ื•ื•ืขืœืŸ ืคืึธืจื•ื™ืก ืื•ืŸ ื ื•ืฆืŸ ืขืก ืคึฟืึทืจ ืื•ื ื“ื–ืขืจ ืกืคึผืึทืจืง ืคึผื™ื™ืคึผืœื™ื™ื ื– ืื•ืŸ ืจืขืงืึธืžืžืขื ื“ืึทื˜ืึธืจ ืกื™ืกื˜ืขืžืขืŸ.

ืขืก ื•ื•ืึธืœื˜ ื–ื™ื™ืŸ ืคื™ื™ึทืŸ ืฆื• ืกื™ื ื’ืงืจืึทื ื™ื™ื– ื“ื™ ื˜ืขืงืข ืกื˜ืึธืจื™ื“ื–ืฉ ืžื™ื˜ ื“ื™ ื“ืึทื˜ืึทื‘ื™ื™ืก ืึทื ืฉื˜ืึธื˜ ืคื•ืŸ ื“ื™ ื˜ืขืงืข ืกื™ืกื˜ืขื. ื“ืึธืก ื–ืึธืœ ื’ืขื‘ืŸ ืื•ื ื“ื– ืงื™ื™ืคืœ ืขื ื“ืคึผืึธื™ื ืฅ ื•ื•ืึธืก ืงืขื ืขืŸ ื ื•ืฆืŸ ื“ื™ ื–ืขืœื‘ืข ื˜ืขืงืข ืกื˜ืึธืจื™ื“ื–ืฉ. ืคึฟืึทืจ ื‘ื™ื™ึทืฉืคึผื™ืœ, ื ื•ืฆืŸ ืงื™ื™ืคืœ ื™ื ืกื˜ืึทื ืกื™ื– ืคึผืจืขืกื˜ืึธ ะธ ืึทื˜ื”ืขื ืึท ืžื™ื˜ ื“ืขืจ ื–ืขืœื‘ื™ืงืขืจ ืงืœื™ื™ ืžืขื˜ืึทืกื˜ืึธืจืข.

ืฆื• ืกืึทืžืขืจื™ื™ื–, ืื™ืš ื•ื•ืึธืœื˜ ื•ื•ื™ ืฆื• ื–ืึธื’ืŸ ืึท ื“ืึทื ืงืขืŸ ื“ื™ืจ ืฆื• ื“ื™ MLFlow ืงื”ืœ ืคึฟืึทืจ ืžืื›ืŸ ืื•ื ื“ื–ืขืจ ืึทืจื‘ืขื˜ ืžื™ื˜ ื“ืึทื˜ืŸ ืžืขืจ ื˜ืฉื™ืงืึทื•ื•ืข.

ืื•ื™ื‘ ืื™ืจ ืฉืคึผื™ืœื˜ ืžื™ื˜ MLflow, ื˜ืึธืŸ ื ื™ื˜ ืงื•ื•ืขื ืงืœืขืŸ ืฆื• ืฉืจื™ื™ึทื‘ืŸ ืฆื• ืื•ื ื“ื– ืื•ืŸ ื–ืึธื’ืŸ ืื•ื ื“ื– ื•ื•ื™ ืื™ืจ ื ื•ืฆืŸ ืขืก, ืื•ืŸ ืืคื™ืœื• ืžืขืจ ืื•ื™ื‘ ืื™ืจ ื ื•ืฆืŸ ืขืก ืื™ืŸ ืคึผืจืึธื“ื•ืงืฆื™ืข.

ื’ืขืคึฟื™ื ืขืŸ ื–ื™ืš ืžืขืจ ื•ื•ืขื’ืŸ ื“ื™ ืงืึธืจืกืึทื–:
ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ. ื™ืงืขืจื“ื™ืง ืงื•ืจืก
ืžืึทืฉื™ืŸ ืœืขืจื ืขืŸ. ืึทื•ื•ืึทื ืกื™ืจื˜ืข ืงื•ืจืก

ืœื™ื™ืขื ืขืŸ ืžืขืจ:

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

ืœื™ื™ื’ืŸ ืึท ื‘ืึทืžืขืจืงื•ื ื’