【機械学習　勉強メモ】機械学習についてインフラでできることを考える。

# はじめに

AWS MLS資格取得を目指して学んだことをまとめます。
機械学習を行うための構築について実践を交えて記載します。
構築で作業時間を少なくできるよう、自動化を行うことを心がけました。

# 機械学習の流れ

まず、機械学習ではどんなことをするのかをまとめます。
この記事では特にインフラに関することに注目します。

・ラベリング
　推論、学習などでデータを使いやすいように整形する。
　分類モデルのためにデータにラベル付けをする。
　大量のデータを保管、処理する。

・開発
　モデルを考えて、コードを書く。
　コードを書くためのツールを用意する。
　ツールを実行するためのリソースを用意する。

・学習
　大量のデータを用いて学習する。
　学習を繰り返してよりよくする。
　リソースのチューニング

・モデル変換
　推論用に学習済みモデルを変換する。

・推論
　学習済みモデルをデプロイして使う。

機械学習としてはデータを用意して、モデルを目標の精度まで学習させることが重要だと思っています。そのため、それ以外のことで頭を使いたくないのではないかと思います。そこで、インフラ面で自動化や時短できないかを考えます。

※参考

https://d1.awsstatic.com/webinars/jp/pdf/services/20190206_AWS_BlackBelt_SageMaker_part1.pdf

# インフラでできることを考える。

構築について実践します。

## IaCで一度やったことを自動化する。

サーバーを構築する方法を考えます。ここでは時間を使いたくないですが、既存の環境に合わせてやる必要があるので大変だと思います。そのため、知見を借りたり一度やったことは自動化したりして、楽にやりたいです。
今回は、以下のことをします。

・CloudFormationでサーバーを作る。
・shellファイルからk8sを構築する。
・k8sのマニフェストファイルからコンテナレジストリを作る。

### CloudFormationでサーバーを作る。

以下、実践していきます。
CloudFormationは基本は過去ののものと同じですが、EC2のボリュームを大きくしました。これから使うコンテナイメージのために大きさが必要になるためです。

https://qiita.com/kokohei39/items/89a975564a513a5025a4

```yaml:CloudFormation用yaml(EC2部分抜粋)
Resources:
  ec2:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: t2.medium
      KeyName: <key-name>
      ImageId: ami-02892a4ea9bfa2192
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'true'
          DeviceIndex: '0'
          SubnetId: !Ref <public-subnet>
          GroupSet:
            - !Ref <security-group>
      BlockDeviceMappings:
        - DeviceName: "/dev/xvda"
          Ebs: 
            VolumeSize: 20
            VolumeType: "gp2"
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-ec2'
```

### shellファイルからk8sを構築する。

EC2にk8s環境を作ります。過去記事でやっているので、同じ方法で行います。初めてk8s環境を作る場合には、公式ページなどを探してエラー対応しながら行う必要があります。この記事では過去記事のものをshellファイルの形で再利用します。

https://qiita.com/kokohei39/items/4a018e74ca851eaeb20e#kubernetes%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

### k8sのマニフェストファイルからコンテナレジストリを作る。

後述でイメージをpushしてPODを作ります。そのため、コンテナレジストリを作成します。いろいろな方法があると思いますが、今回はローカルに作成します。ローカルにk8s環境がある場合、コンテナで用意する方法があります。そのためのコンテナイメージは公開されているものがあり、それを使うだけでよさそうです。今回はPODとして作ります。

https://qiita.com/Brutus/items/da63d23be32d505409c6

先にPOD用のnamespaceを作成します。

```yaml:namespace.yaml
kind: Namespace
apiVersion: v1
metadata:
  name: myns
```

```bash
kubectl apply -f namespace.yaml
```

PODのyamlは以下の通りです。

```yaml:deployment-registry.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: myns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
      - name: registry
        image: docker.io/registry:2.8.1
        volumeMounts:
        - name: registry-config
          mountPath: /etc/docker/registry
        - name: registry-storage
          mountPath: /var/lib/registry
      - name: registry-ui
        image: docker.io/joxit/docker-registry-ui:2.2.1
        envFrom:
        - configMapRef:
            name: registry-ui-config
      volumes:
        - name: registry-config
          configMap:
            name: registry-config
        - name: registry-storage
          hostPath:
            path: /home/ec2-user/registry
            type: DirectoryOrCreate
```

以下、yamlの設定について記載します。

・コンテナ

|コンテナ名|メモ|
|---|---|
|registry|コンテナレジストリ|
|registry-ui|ブラウザからコンテナイメージを確認できるようにする|

・ボリューム

|ボリューム名|マウントするコンテナ|メモ|
|---|---|---|
|registry-config|registry|コンテナレジストリの設定ファイルを格納する|
|registry-storage|registry|コンテナイメージのローカルの保存先|

コンテナレジストリの設定にはConfigMapを使っています。以下、ConfigMapのyamlです。

```yaml:configmap-registry.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: registry-config
  namespace: myns
data:
  config.yml: |
    version: 0.1
    log:
      fields:
        service: registry
    storage:
      cache:
        blobdescriptor: inmemory
      filesystem:
        rootdirectory: /var/lib/registry
    http:
      addr: :5000
      headers:
        X-Content-Type-Options: [nosniff]
        Access-Control-Allow-Origin: ['*']
        Access-Control-Allow-Headers: ['*']
    health:
      storagedriver:
        enabled: true
        interval: 10s
        threshold: 3
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: registry-ui-config
  namespace: myns
data:
  REGISTRY_URL: http://localhost:30500
  REGISTRY_TITLE: "local registry"
```

URLとポートは後述でまとめます。
作成したyamlファイルを使って、PODを作成します。
作成後、ブラウザからコンテナレジストリにアクセスしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/a64f7f47-2c85-ac71-228f-bea1eb147f4d.png)

まだ、コンテナイメージをpushしていないので、何も表示されません。もし、pushされていてレジストリのPODがダウンしたとしても、DeploymentによるPODの再起動と、ローカルに保存したイメージの読み込みが行われます。

## Jupyterコンテナイメージを使ってインストールの手間を省く。

次にコードを書くための方法として、Jupyterを用意します。コンテナレジストリと同様にコンテナイメージが公開されているため、利用します。
Jupyterを動かすPODのyamlファイルを作成します。DeploymentにJupyterイメージを指定するだけで完了です。

```yaml:deployment-jupyter.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jupyter
  namespace: myns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jupyter
  template:
    metadata:
      labels:
        app: jupyter
    spec:
      containers:
      - name: jupyter
        image: docker.io/jupyter/base-notebook:python-3.10.4
```

PODを作成し、ブラウザからアクセスしてみます。
Jupyterに初めてアクセスするにはトークンが必要であるため、起動ログから確認します。

```bash
$ JUPYTER_POD=`kubectl get pod -n myns | grep jupyter | awk '{print $1}'`
$ kubectl logs -n myns $JUPYTER_POD
・・・
    To access the server, open this file in a browser:
        file:///home/jovyan/.local/share/jupyter/runtime/jpserver-7-open.html
    Or copy and paste one of these URLs:
        http://jupyter-7f5b78d4cf-qjbhb:8888/lab?token=<token>
     or http://127.0.0.1:8888/lab?token=<token>
・・・
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/b493a4e8-7c6f-c049-dac2-45daa5a68f9e.png)

アクセスできました。
初めて使いました。スクリプトの実行とマークダウンが使えるんですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/bc42353c-0b8b-c8d6-87b9-ca0291e31f02.png)

※参考

https://qiita.com/namoshika/items/63db972bfd1030f8264a

## データの整形を行うためFluentdを用意する。

学習、推論用のデータを用意する方法を考えます。データに対して以下の処理を行うことを考えます。
・整形：モデルを扱える形にする。
・分類：データを加工してラベル付けする。
・フィルタリング：意図しないデータを省く。

これらを行うための方法にFluentdがあるので、作ってみます。また、Fluentdの設定ファイルを作るのは大変なので、負担を減らすためFluentdのGUI機能も使ってみます。

まず、コンテナイメージを作成します。
以下のコマンドを実行します。

```bash
#!/bin/bash -ex

REGISTRY_HOST='localhost:30500'

echo 'install git'
sudo yum install git -y

echo 'build fluentd-ui'
COMMIT_ID='d48d672ebc505b7a2ac4ff35afdbb8f843a62ef3'
FLUENTD_UI_VERSION='v1.2.2'
LOCAL_REPO_DIR="$HOME/fluentd-ui"
git clone https://github.com/fluent/fluentd-ui.git $LOCAL_REPO_DIR
cd $LOCAL_REPO_DIR
git checkout $COMMIT_ID
IMAGE=$REGISTRY_HOST/fluent/fluentd-ui:$FLUENTD_UI_VERSION
sudo docker build -t $IMAGE $LOCAL_REPO_DIR
sudo docker push $IMAGE
```

やっていることは以下の通りです。
・gitのインストール
・ビルドするためのソースファイルのあるgitリポジトリのclone
・fluentd-uiのbuild、push

コンテナレジストリはServiceを使いポート30500を使っているため、build,push時に指定するようにします。詳細は後述します。

pushしたイメージを使ってFluentdのPODを作成するyamlファイルを作成します。

```yaml:deployment-fluentd.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fluentd
  namespace: myns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      containers:
      - name: fluentd
        image: localhost:30500/fluent/fluentd-ui:v1.2.2
```

PODの作成後、ブラウザからアクセスしてみます。
初期ログイン情報は以下の通りです。
・アカウント名：admin
・パスワード：changeme

https://github.com/fluent/fluentd-ui

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/3f3125a8-005b-4d0f-935c-7abd8bb7c28e.png)

アクセスできました。
ブラウザから、設定ファイルの編集やログの閲覧ができるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/62428a45-90d1-4327-308f-3f8ba06f449c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/213ddf5a-c263-09e5-7e6e-b594cac440cf.png)

さらにデータの図や検索など行うために、Fluentd+Elasticsearch+Kibanaを組み合わせることができます。

https://blog.idcf.jp/entry/elasticsearch1

## 処理の進捗を管理するJenkinsを用意する。

学習するときのことを想像します。大量のデータを使って長時間処理することになるのではないでしょうか。これを自動化できれば開発者の負担が減ると思います。
Jenkinsでは以下のことができるようになります。
・コードのまとまりをジョブとして、繰り返し定期的に実行できる。
・GitHubのプラグインがあり、ソースを管理しやすくなる。
・通知機能があり、問題があったときに気づきやすい。

Jenkinsもコンテナイメージが公開されているので、やってみます。
JenkinsのPODを作成するyamlファイルを作成します。

```yaml:deployment-jenkins.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: myns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      containers:
      - name: jenkins
        image: docker.io/jenkins/jenkins:jdk11
```

次にPODを作成し、ブラウザからアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/fe7bb758-c0d8-f551-a70e-4815b27f4621.png)

初期ログインパスワードは`/var/jenkins_home/secrets/initialAdminPassword`に記載があるようです。

```
$ JENKINS_POD=`kubectl get pod -n myns | grep jenkins | awk '{print $1}'`
$ kubectl exec -n myns $JENKINS_POD -- cat /var/jenkins_home/secrets/initialAdminPassword
```

ログイン後、GitHubを含めて色々なプラグインがインストールできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/6a5b045c-bc46-e080-e240-385b75d738e4.png)

通知機能として、メールがあり、「Jenkinsの管理」->「システムの設定」から設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/6498b287-1504-e824-f7fa-bda36ebfdd62.png)

試しにメールを送信してみると、以下のようなメールが届きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/6c9c038d-7b0a-1f8c-362c-6ce87cb02744.png)

## リソースを視覚的に確認するためにGrafanaを用意する。

学習を進めていくうちに、扱うデータ量が増え、処理が多くなった状況を想像します。
そうなるとサーバーのCPUやメモリが足りなくなるかもしれません。処理が中断される前に、リソースを増やしたり処理を少なくしたりなど対策したいと思います。
そこで、リソースを監視する方法としてGrafanaを用意します。
Grafanaでは以下のことができます。
・メトリクスを視覚的に表示する。
・メトリクスに対するアラートを出す。
GrafanaはOperatorを使うとインストールが楽なのでやってみます。

以下のコマンドを実行します。

```bash:setup-helm-and-prometheus.sh
#!/bin/bash -ex
SCRIPT_DIR=$(cd $(dirname $0); pwd)

echo 'install helm'
HELM_VERSION='v3.8.1'
cd $HOME
curl "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" -o helm.tar.gz
tar -zxvf helm.tar.gz
mv linux-amd64 helm
echo 'export PATH=$PATH:$HOME/helm' >> $HOME/.bashrc
source .bashrc
helm version

echo 'install prometheus operator'
cd $SCRIPT_DIR
PROMETHEUS_OPERATOR_VERSION='36.0.0'
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--version $PROMETHEUS_OPERATOR_VERSION \
--create-namespace \
-n monitoring \
-f prometheus/prometheus-operator-values.yaml
```

```yaml:prometheus/prometheus-operator-values.yaml
grafana:
  service:
    type: NodePort
    port: 3000
    targetPort: 3000
    nodePort: 30300

prometheus:
  service:
    type: NodePort
    port: 9090
    targetPort: 9090
    nodePort: 30090
```

やっていることは以下の通りです。
・Helmのインストール
・Prometheus Operatorのインストール

また、`helm install`時にyamlファイルをしていますが、これはGrafanaを公開するServiceのタイプをNodePortにするためです。詳細は後述します。
それでは、実際にアクセスしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/1d50b7b2-6ca3-300a-9cc6-0c0494f6ef98.png)

初期ユーザー名とパスワードですが、k8s環境のSecretに記載してあります。

```
$ kubectl describe secret -n monitoring kube-prometheus-stack-grafana
Name:         kube-prometheus-stack-grafana
Namespace:    monitoring
Labels:       app.kubernetes.io/instance=kube-prometheus-stack
              app.kubernetes.io/managed-by=Helm
              app.kubernetes.io/name=grafana
              app.kubernetes.io/version=8.5.3
              helm.sh/chart=grafana-6.29.6
Annotations:  meta.helm.sh/release-name: kube-prometheus-stack
              meta.helm.sh/release-namespace: monitoring

Type:  Opaque

Data
====
admin-password:  13 bytes
admin-user:      5 bytes
ldap-toml:       0 bytes
$ kubectl -n monitoring get secret kube-prometheus-stack-grafana -o jsonpath="{.data.admin-user}" | base64 --decode
*** #username
$ kubectl -n monitoring get secret kube-prometheus-stack-grafana -o jsonpath="{.data.admin-password}" | base64 --decode
*** #Password
```

デフォルトですでにダッシュボードが作成されており、今まで作成したPODでどれくらいメモリー、CPUが使われているかがわかります。

![localhost_3000_d_8b7a8b326d7a6f1f04244066368c67af_kubernetes-networking-namespace-pods_orgId=1&refresh=10s&var-datasource=default&var-cluster=&var-namespace=All&var-resolut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/cb2fbddc-33cf-7432-a053-1a564b04fac5.png)

また、contact pointとアラートルールを作成することで、アラート機能を利用できます。アラート先にはEmail,Microsoft Teamsなどが指定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/fd46a044-53c5-b678-3046-4102f5a7e2e7.png)


そのほかにPrometheusも使えます。

![localhost_9090_graph_g0.expr=node_namespace_pod_container%3Acontainer_cpu_usage_seconds_total%3Asum_irate&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=15m&g0.end_input=2022-06-11%2007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/66cf1906-cc2f-74d6-be74-52a06ed6cd7b.png)

# （参考）ローカルPCのブラウザからPODにアクセスする方法

## 概要

今回、作成したPODにはローカルPCのブラウザからアクセスしました。方法としてはSSHポートフォワーディングとServiceのNodePortを組み合わせています。
まず、ServiceのNodePortでPOD内のコンテナでアクセス待ちしているポートをノードのポートに割り当てています。ノードにはローカルPCでSSH接続できるものとし、それ以外のポートは空いていない状況です。そのため、SSHポートフォワーディングで、コンテナの割り当てたポートにアクセスできるようにしています。
ブラウザからコンテナイメージへアクセスする経路イメージは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/ced9dee8-e4c8-643e-f99d-343c6e3a841c.png)

ローカルPCではSSHでEC2に接続していますが、Serviceによりregistry-uiコンテナにポート80でアクセスされます。
レスポンスはローカルPCのポート80で待ち受けされます。ブラウザのURLを`http://localhost:80`とすることでアクセスできます。

## NodePortで割り当てるポート

コンテナで使用するポートに合わせ、ServiceにてNodePortを設定する必要があります。以下、ノードと割り当てるコンテナのポートをまとめます。

|ノードのポート番号例|割り当てるポート番号|メモ|
|---|---|---|
|30500|registryコンテナ5000ポート|コンテナイメージのpushのため|
|30080|registryコンテナ80ポート|ブラウザからアクセス|
|30001|jupyterコンテナ8888ポート|ブラウザからアクセス|
|30002|jenkinsコンテナ8080ポート|ブラウザからアクセス|
|30003|fluentdコンテナ9292ポート|ブラウザからアクセス|
|30300|grafanaコンテナ3000ポート|ブラウザからアクセス|
|30090|prometheusコンテナ9090ポート|ブラウザからアクセス|

また、コンテナレジストリのServiceのyaml例を示します。

```yaml:service-registry.yaml
apiVersion: v1
kind: Service
metadata:
  name: registry-service
  namespace: myns
spec:
  type: NodePort
  ports:
    - name: "registry"
      protocol: "TCP"
      port: 25000
      targetPort: 5000
      nodePort: 30500
    - name: "registry-ui"
      protocol: "TCP"
      port: 20080
      targetPort: 80
      nodePort: 30080
  selector:
    app: registry
```

## SSHポートフォワーディングの方法

割り当てられたノードのポート番号を使って、SSHポートフォワーディングをします。
Teratermの機能を使うとSSH接続の設定を省略でき、少し楽です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/ec6a8e50-3ade-cde2-cb16-c4885034bf6a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/6c148d8a-7f90-6bae-b6cd-005839a9f56f.png)

Teratermが接続された状態で必要な情報を入力します。
1個のTeratermで複数のポートフォワーディングの設定を行うことができます。ローカルのポートはブラウザにて入力するポートに相当します。
毎回設定するのは面倒な時は、Teratermの現在の設定をINIファイルに保存し、起動時に読み込むようにすることもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/45ec7a2a-1075-ad55-22e9-a85e357c9749.png)

# これから勉強すること

まだ機械学習で学べていないことは多いです。以下、今後勉強していきたいと思います。
・分散処理構成について（Spark）
・AWS SageMaker
・学習の性能評価
・モデルパラメータチューニング

# おわりに

今回触れた機能ですが、AWSでも実現できると思います。AWSではミドルウェアを独自に作っていたりマネージドサービスとして提供されたりしています。
AWSは強いなと感じる今日この頃です。使いこなせるように勉強していきたいです。