2023年11月30日木曜日

Gitlab の alertmanager でテンプレートファイルを使用する方法

Gitlab の alertmanager でテンプレートファイルを使用する方法

概要

前回メール本文のテンプレートをカスタマイズする方法を紹介しました
今回は直接テンプレートを docker-compose に記載せずテンプレートファイルを使ってカスタムしてみたいと思います

環境

  • Gitlab 16.3.6
  • Alertmanager 0.25.0

my_email_html.tmpl

まずは html メールのカスタマイズからしていきます
html テンプレートのベースはこれを使います
リンクの部分を削除するのとテンプレート名を「my.email.html」という名前に変更しています

  • vim my_email_html.tmpl
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "my.email.html" }}
<!--
Style and HTML derived from https://github.com/mailgun/transactional-email-templates


The MIT License (MIT)

Copyright (c) 2014 Mailgun

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ template "__subject" . }}</title>
<style>
@media only screen and (max-width: 640px) {
  body {
    padding: 0 !important;
  }

  h1,
h2,
h3,
h4 {
    font-weight: 800 !important;
    margin: 20px 0 5px !important;
  }

  h1 {
    font-size: 22px !important;
  }

  h2 {
    font-size: 18px !important;
  }

  h3 {
    font-size: 16px !important;
  }

  .container {
    padding: 0 !important;
    width: 100% !important;
  }

  .content {
    padding: 0 !important;
  }

  .content-wrap {
    padding: 10px !important;
  }

  .invoice {
    width: 100% !important;
  }
}
</style>
</head>

<body itemscope itemtype="https://schema.org/EmailMessage" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;">

<table class="body-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
  <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
    <td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
    <td class="container" width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;" valign="top">
      <div class="content" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;">
        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="#fff">
          <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            {{ if gt (len .Alerts.Firing) 0 }}
            <td class="alert alert-warning" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;" valign="top" align="center" bgcolor="#E6522C">
              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
                {{ .Name }}={{ .Value }}
              {{ end }}
            </td>
            {{ else }}
            <td class="alert alert-good" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;" valign="top" align="center" bgcolor="#68B90F">
              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
                {{ .Name }}={{ .Value }} 
              {{ end }}
            </td>
            {{ end }}
          </tr>
          <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            <td class="content-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;" valign="top">
              <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                  </td>
                </tr>
                {{ if gt (len .Alerts.Firing) 0 }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Firing | len }}] Firing</strong>
                  </td>
                </tr>
                {{ end }}
                {{ range .Alerts.Firing }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                  </td>
                </tr>
                {{ end }}

                {{ if gt (len .Alerts.Resolved) 0 }}
                  {{ if gt (len .Alerts.Firing) 0 }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    <hr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  </td>
                </tr>
                  {{ end }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
                  </td>
                </tr>
                {{ end }}
                {{ range .Alerts.Resolved }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                  </td>
                </tr>
                {{ end }}
              </table>
            </td>
          </tr>
        </table>

        <div class="footer" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;">
          <table width="100%" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            </tr>
          </table>
        </div></div>
    </td>
    <td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
  </tr>
</table>

</body>
</html>

{{ end }}

テンプレートファイルの配置

コンテナで動作する Gitlab が参照できるようにします
今回はホストマシンの一部のパスをマウントしてコンテナが参照できるようにしています

  • mkdir /path/to/opt/alertmanager/template
  • cp my_email_html.tmpl /path/to/opt/alertmanager/template

gitlab.rb

あとはテンプレートを参照するように alertmanager の設定を書き換えます

html の部分を定義した template を使うように変更します
また配置したテンプレートファイルを読み込むように alertmanager['templates'] を定義します

  • vim .gitlab.rb
alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $$alerts := "" }}{{ $$instance_id := (index .Alerts 0).Labels.instance_id }}{{ range .Alerts }}{{ $$alerts = (printf "%v %v" $$alerts .Labels.alertname) }}{{ end }}{{ $$instance_id }} ->{{ $$alerts }})'
      },
      html: '{{ template "my.email.html" . }}',
      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]
alertmanager['templates'] = ['/var/opt/gitlab/alertmanager/template/*.tmpl']
# alertmanager['default_receiver'] = 'email'

動作確認

あとは redis などを停止すればアラートがきます

  • docker-compose exec gitlab gitlab-ctl stop redis

こんな感じで alertmanager へのリンクが削除されているのが確認できると思います

テキストメールのテンプレート化

次にテキストメールでもテンプレート化してみます
テンプレート化する内容は前回の本文とタイトルの内容をテンプレート化してみます

今回は1つのテンプレートファイルに2つの define を使って本文とタイトル用のテンプレートを定義します

  • vim my_email_text.tmpl
{{ define "my.email.text.body" }}
{{ range .Alerts.Firing }}
{{ range .Annotations.SortedPairs }}
- {{ .Name }} = {{ .Value }}
{{ end }}
{{ end }}
{{ end }}

{{ define "my.email.text.subject" }}
{{ $alerts := "" }}
{{ $instance_id := (index .Alerts 0).Labels.instance_id }}
{{ range .Alerts }}
{{ $alerts = (printf "%v %v" $alerts .Labels.alertname) }}
{{ end }}
[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $instance_id }} ->{{ $alerts }})
{{ end }}

コンテナが参照できるパスにコピーします

  • cp my_email_text.tmpl /path/to/opt/alertmanager/template

gitlab.rb でテキストメールの本文とタイトルにテンプレートを使うように変更します
html メールは無効化するのでブランクを設定します

  • vim gitlab.rb
alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '{{ template "my.email.text.subject" . }}'
      html: '',
      text: '{{ template "my.email.text.body" . }}',
      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]
alertmanager['templates'] = ['/var/opt/gitlab/alertmanager/template/*.tmpl']
# alertmanager['default_receiver'] = 'email'

あとは再起動して再度 redis を停止すればテンプレートファイル化された内容でテキストメールのアラートが届くことが確認できると思います

最後に

docker-compose に直接テンプレート構文を使うこともできますがテンプレートの内容が長くなり一行では管理しづらい場合などはテンプレートファイル化してあげるといいのかなと思います

参考サイト

2023年11月29日水曜日

Gitlab の alertmanager でメール本文をカスタマイズする方法

Gitlab の alertmanager でメール本文をカスタマイズする方法

概要

前回 routes を設定する方法を紹介しました
今回はメールの本文やタイトルをカスタムする方法を紹介します

環境

  • Gitlab 16.3.6
  • Alertmanager 0.25.0

gitlab.rb

alertmanager['receivers'] の email_configs 内にある headers.subject, text を編集します
デフォルトの html メールは無効にするためブランクを設定しておきます

メールの内容には golang のテンプレート構文が使えます
注意するのは変数の定義で golang ではダラー1つで定義しますが今回は docker-compose 内でテンプレートを定義するためダラーを2つにする必要があります

使用できるテンプレート文字列やテンプレート構文に関しては各種公式を参考にしてください

alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $$alerts := "" }}{{ $$instance_id := (index .Alerts 0).Labels.instance_id }}{{ range .Alerts }}{{ $$alerts = (printf "%v %v" $$alerts .Labels.alertname) }}{{ end }}{{ $$instance_id }} ->{{ $$alerts }})'
      },
      html: '',
      text: '{{ range .Alerts.Firing }}{{ range .Annotations.SortedPairs }}  - {{ .Name }} = {{ .Value }}{{ "\n" }}{{ end }}{{ end }}',

      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]

custom.rules

groups:
- name: Custom
  rules:
  - alert: RedisDown
    expr: avg_over_time(redis_up[5m]) * 100 < 50
    labels:
      severity: critical
      instance_id: hoge
    annotations:
      description: The Redis service is not responding for more than 50% of the time for 5 minutes.
      summary: The Redis service is not responding

動作確認

実際に redis をダウンさせるとカスタムした本文とタイトルでテキストメールが届くと思います

最後に

Gitlab 内の alertmanager のメール本文をカスタムする方法を紹介しました
golang の知識が必要になると alertmanager に送信させる json の内容も把握する必要があります

alertmanager に送信される json 情報はどこかで確認することができるのだろうか

参考サイト

2023年11月28日火曜日

amtool を使って alertmanager のテンプレートの確認をする方法

amtool を使って alertmanager のテンプレートの確認をする方法

概要

メールなどを送信する際に事前にテンプレートの内容がちゃんと展開できるか確認することができます

環境

  • Ubuntu 22.04
  • amtool 0.26.0

インストール

  • go install github.com/prometheus/alertmanager/cmd/amtool@latest

data.json

{
  "Status": "firing",
  "Alerts": [
    {
      "Status": "firing",
      "Labels": {
        "alertname": "alert1",
        "instance_id": "hoge"
      },
      "Annotations": {
        "runbook": "https://1.example.com/",
        "description": "desc1"
      }
    },
    {
      "Status": "firing",
      "Labels": {
        "alertname": "alert2",
        "instance_id": "hoge"
      },
      "Annotations": {
        "runbook": "https://2.example.com/",
        "description": "desc2"
      }
    }
  ],
  "GroupLabels": {
    "cluster": "cluster-A"
  }
}

試す

template.text に好きなテンプレート文字列を渡してテストできます
テスト用のデータは template.data で渡します

  • ~/go/bin/amtool template render --template.glob=/dev/null --template.text='{{ $alerts := "" }}{{ $instance_id := (index .Alerts 1).Labels.instance_id }}{{ range .Alerts }}{{ $alerts = (printf "%v %v" $alerts .Labels.alertname) }}{{ end }}{{ $instance_id }} ->{{ $alerts }}' --template.data=data.json

結果は以下のようになります

hoge -> alert1 alert2

最後に

サンプルは golang のテンプレートの記述に慣れていればもっと簡潔に書けるかなと思います

書いているときに一行だと大変なので複数行で記載してから一行にするといいかなと思います

参考サイト

2023年11月27日月曜日

Mac でファイルを共有する方法

Mac でファイルを共有する方法

概要

ざっくりメモ

環境

  • M2Mac mini (共有する側
  • Mac Book Air (共有される側

共有フォルダの指定

M2Mac mini 上での作業

  • 環境設定からファイル共有
  • 共有フォルダの指定
  • 共有するユーザの追加と権限の確認

共有フォルダにアクセス

Mac Book Air 上での作業

  • Finder
  • ネットワーク
  • M2Mac mini があることを確認
  • ダブルクリックし接続
  • 共有したフォルダにアクセスできることを確認

トラブルシューティング

  • 同一ネットワーク上にいるか
  • 共有したいユーザの設定があっているか、権限はあっているか

2023年11月24日金曜日

Gitlab の Promethues に独自のルールを設定する方法

Gitlab の Promethues に独自のルールを設定する方法

概要

rules ファイルを作成してコンテナ側にマウントすれば OK です
今回は簡単な独自ルールを作成して設定してみました

環境

  • Gitlab 16.3.6

custom.rules

groups:
- name: MyCustomRule
  rules:
  - alert: CpuHighUsage
    expr: instance:node_cpu_utilization:ratio > 0.5
    for: 30m
    annotations:
      description: Current cpu usage is {{ $value }}.
      summary: CPU usage exceeded 50%.

instance:node_cpu_utilization:ratio は Gitlab のルールでデフォルトで提供されている Recoding rule の一つで CPU の使用率を取得することができる値です

コンテナ内の /var/opt/gitlab/prometheus/rules/ に配置

  • cp custom.rules /mnt/opt/gitlab/prometheus/rules

ローカル領域のマウントでもボリュームのマウントでも OK です

Omnibus Gitlab のデフォルトでは /var/opt/gitlab/prometheus/rules/ 配下のルールを見るのでそこに配置しましょう

gitlab.rb

もし別の場所に rules ファイルを配置する場合は配列に追加しましょう

prometheus['rules_files'] = ['/var/opt/gitlab/prometheus/rules/*.rules']

動作確認

  • docker-compose down
  • docker-compose up -d

であとは Promethues の rules の設定を確認して追加されていることを確認しましょう

参考サイト

2023年11月23日木曜日

Gitlab の Alertmanager で特定のアラートだけメールで送信する方法

Gitlab の Alertmanager で特定のアラートだけメールで送信する方法

概要

前回はとりあえず出たアラートをすべてメールで送信しました
今回は route 機能を使って特定のアラートだけをメールで飛ばすようにしてみます

環境

  • Gitlab 16.3.6
  • Alertmanager 0.25.0

gitlab.rb

alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'alertname = RedisDown' ]
  }
]
# alertmanager['default_receiver'] = 'email'

ポイント

  • alertname は必ず付与されるのでこれを使ってアラートを特定します
    • マッチには正規表現が使える match_re などもあります
    • ルールをカスタムすれば severity などのラベルも付与できますが面倒なので alertname を使います
    • デフォルトの Gitlab のルールだと他に instance, job あたりがマッチルールに使えます
  • default_receiver を設定してしまうと route にマッチしない場合に defualt_receiver の設定を使ってしまうのでコメントする

参考サイト

2023年11月22日水曜日

BeautifulSoup を使った画像URL取得のサンプルコード

BeautifulSoup を使った画像URL取得のサンプルコード

概要

img タグを取得してその src 属性を取得するだけです

環境

  • macOS 14.0
  • Python 3.11.6

サンプルコード

import requests
from bs4 import BeautifulSoup


class Downloader:
    def __init__(self, url: str = "https://picsum.photos/"):
        super().__init__()
        self.url = url

    def get_img_urls(self) -> list:
        response = None
        try:
            with requests.Session() as session:
                response = session.get(self.url, timeout=60)
                if response.status_code == 200:
                    soup = BeautifulSoup(response.text, "html.parser")
                    img_urls = [element.get("src") for element in soup.find_all("img")]
                    return img_urls
        except Exception as e:
            print(e)
            if response:
                response.close()
        return []


if __name__ == "__main__":
    dl = Downloader()
    for img_url in dl.get_img_urls():
        print(img_url)

2023年11月21日火曜日

Chromeでパスワードを一括削除する方法

Chromeでパスワードを一括削除する方法

概要

最近の chrome だと履歴から削除できなくなっています
とりあえずローカルだけでも削除したい場合には以下の方法が簡単です

環境

  • Windows 10
  • Chrome 119.0.6045.160

同期をオフにする

Chrome で以下にアクセスし同期設定でパスワードだけ無効にします

chrome://settings/syncSetup/advanced

Login Data ファイルの削除

%LocalAppData%\Google\Chrome\User Data\Default\ にある Login Data を削除します

動作確認

Chrome を再起動してパスワードが補完されないことを確認しましょう

最後に

根本的には Google のパスワードマネージャに保存されているデータを削除する必要がありますがなぜかパスワードマネージャはパスワードを一件ずつしか削除できないのでこの方法で無理やりローカルだけ削除しています

ちなみに同期を有効にしてしまうと再度ローカルにパスワードファイルが生成されてしまいます

また執筆時点での Chrome ではパスワードのオートフィルを無効にする機能がないのでパスワード自体を削除しないと勝手に補完してしまいます

参考サイト

2023年11月20日月曜日

Gitlab の Alertmanager を使ってメール通知する方法

Gitlab の Alertmanager を使ってメール通知する方法

概要

過去に slack 通知する方法を紹介しました
今回はメール通知する方法を紹介します

環境

  • Gitlab 16.3.6

gitlab.rb の設定

alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      send_resolved: true
    ]
  }
]
alertmanager['default_receiver'] = 'email'

docker-compose.yml の設定

9093 を外部から見れるようにするには ports を設定してホスト側にもバインドしてあげましょう

ports:
  - 9093:9093

動作確認

  • docker-compose down
  • docker-compose up -d

で適用し

  • docker-compose exec gitlab gitlab-ctl stop redis

すると以下のようなメールが来ることが確認できると思います

Alertmanager でもアラートを確認できると思います

トラブルシューティング

2023-11-17_04:52:18.74549 ts=2023-11-17T04:52:18.745Z caller=notify.go:732 level=warn component=dispatcher receiver=email integration=email[0] msg="Notify attempt failed, will retry later" attempts=1 err="'require_tls' is true (default) but \"smtp.ess.nifcloud.com:465\" does not advertise the STARTTLS extension"

こんな感じのエラーが Alertmangaer から出ている場合は require_tls を false にしてください


An error has occurred while serving metrics:

collected metric pg_replication_is_replica label:{name:"server"  value:"/var/opt/gitlab/postgresql:5432"}  gauge:{value:0} has help "Indicates if this host is a slave" but should have "Indicates if the server is a replica"

という感じで postgres_exporter がエラーになっている場合は postgres_exporter を無効にすればとりあえずアラートは解消されます

postgres_exporter['enable'] = false

参考サイト

2023年11月18日土曜日

PydanticV2 で dict から dataclass を生成する方法

PydanticV2 で dict から dataclass を生成する方法

概要

dict -> obj のデシリアライズを Pydantic V2 を使ってやる方法を紹介します
pydantic の dataclass を使います

過去に JSONWizard を使った方法も紹介しています

環境

  • Python 3.10.2
  • pydantic 2.5.1

サンプルコード

from dataclasses import asdict

from pydantic.dataclasses import dataclass


@dataclass
class Profile:
    lang: str
    framework: str


@dataclass
class User:
    id: int
    name: str
    profile: Profile


if __name__ == "__main__":
    # dict -> obj
    user_dict = {
        "id": 1,
        "name": "hawksnowlog",
        "profile": {
            "lang": "python",
            "framework": "flask",
        },
    }
    user = User(**user_dict)
    print(user.id)
    print(user.name)
    print(user.profile.lang)
    print(user.profile.framework)
    # obj -> dict
    print(asdict(user))

ちょっと解説

  • ネストした構造でもいけます
  • Optional など組み合わせれば任意の値もとれます
  • BaseModel ではこの方法は使えません
  • 各種フィールドは Field として定義することはできません

最後に

BaseModel として使えないのが少し残念ですが単純な dict との変換ならこれが一番簡単な方法かなと思います

ただサードパーティを使うので管理などは少し面倒です
本当に簡単な変換なのであれば純正の json モジュールだけでも何とかなります

参考サイト

2023年11月17日金曜日

rqscheduler でエンキューする際にキーワード引数を指定するサンプル

rqscheduler でエンキューする際にキーワード引数を指定するサンプル

概要

kwargs を使ってワーカー側は受け取ることができるので特定の enqueue_in を判定するときなどに使えそうです

環境

  • Python 3.11.3
  • rq-scheduler 0.13.1

サンプルコード

from datetime import timedelta

from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

from my_lib.util import Message

queue = Queue("default", connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)


scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello")
  • vim my_lib/util.py
class Message:
    def say(self, **kwargs):
        print(kwargs)

最後に

AttributeError: Can't get attribute 'Message' on <module '__main__' from '/home/user01/.local/share/virtualenvs/python-T6UYzsfV/bin/rqscheduler'>

という感じのエラーになる場合は enqueue_in する際のメソッドを素直にモジュール化しましょう
今回であれば my_lib/__init__.py の設置も忘れずに行いましょう

2023年11月16日木曜日

Pydantic で Union を使って model_validate する場合は必ず discriminator を使うべきである

Pydantic で Union を使って model_validate する場合は必ず discriminator を使うべきである

概要

model_validate を使用すると pydantic が自動で型を推論します
更に Union を使っている場合は Union に指定している複数のクラスのどれにするかを自動的に推論します
同じフィールドを持つ場合には意図しないクラスに割り当てられることもあります
そういったことを避けるために discriminator という機能を使いましょう

環境

  • Python 3.10.2
  • pydantic 2.5.1

サンプルコード

from typing import Annotated, Literal, Union

from pydantic import BaseModel, Field, field_validator


class Cat(BaseModel):
    # model_validate 時の型推論用フィールド
    animal_type: Literal["cat"] = "cat"
    value: str = Field()

    @field_validator("value")
    @classmethod
    def validate_value(cls, v: str):
        if v != "neko":
            raise ValueError()
        return v


class Dog(BaseModel):
    # model_validate 時の型推論用フィールド
    animal_type: Literal["dog"] = "dog"
    value: str = Field()

    @field_validator("value")
    @classmethod
    def validate_value(cls, v: str):
        if v != "inu":
            raise ValueError()
        return v


Animal = Annotated[
    Union[Cat, Dog],
    Field(discriminator="animal_type"),
]


class Trainer(BaseModel):
    # model_validate 時の型推論用フィールド
    animal: Animal = Field()


if __name__ == "__main__":
    # 成功
    cat = Cat(value="neko")
    dog = Dog(value="inu")
    trainer1 = Trainer(animal=cat)
    trainer2 = Trainer(animal=cat)
    Trainer.model_validate(trainer1.model_dump())
    Trainer.model_validate(trainer2.model_dump())
    # 失敗
    cat = Cat(value="inu")
    trainer1 = Trainer(animal=cat)
    Trainer.model_validate(trainer1.model_dump())

ちょっと解説

一見当たり前のことをしていますが同一のフィールド名をもつクラスを作成しそのクラスに対して model_validate を実行するとフィールドの情報からクラスを推測するため間違ったクラスになる可能性があります

model_dump はクラスの json 情報を返却します
また model_validate で渡す json 情報はすべてのフィールドがプリミティブな値でなければいけないので model_dump される json はすべて文字列だけの情報になります

その状態で discriminator 情報なしで model_validate するとクラスを特定する情報がない状態になってしまいます
なのでクラスを特定する情報として今回 animal_type フィールドを追加し必ずクラスごとに一意になるように値を設定することでプリミティブな値のみを持つ model_dump の情報からでも適切なクラスに変換し model_validate することができるようになります

最後に

Pydantic で Union を使う場合には必ず discriminator を使うようにしましょう

参考サイト

2023年11月15日水曜日

MySQL で csv 出力する方法

MySQL で csv 出力する方法

概要

into outfile はいろいろと面倒なので into outfile を使わずに csv にする方法を紹介します
db_name と table_name は自身の環境に合わせて変更してください
またカラム名に予約語が入っていることを考慮して内部で生成している SQL をバッククオートで囲んでいます

環境

  • MySQL 5.6.22

SQL

SET
  @VTable = 'table_name'
;
SET
  @VAllCols = CONCAT('SELECT CONCAT(',(
      SELECT
        CONCAT('\`',(
            SELECT
              GROUP_CONCAT(COLUMN_NAME SEPARATOR '\`,\', \',\`')
            FROM
              information_schema.columns
            WHERE
              TABLE_NAME = @VTable
            GROUP BY
              table_name
          ), '\`) FROM ', @VTable, ';')
    ))
;
PREPARE stmt
FROM
  @VAllCols
;
EXECUTE stmt
;
DEALLOCATE PREPARE stmt
;

シェルから1行で実行したい場合

mysql -u user_name -p db_name -h 192.168.100.1 -e "SET @VTable = 'table_name'; SET @VAllCols = CONCAT('SELECT CONCAT(',(SELECT CONCAT('\`',(SELECT GROUP_CONCAT(COLUMN_NAME SEPARATOR '\`,\',\',\`') FROM information_schema.columns WHERE TABLE_NAME = @VTable GROUP BY table_name),'\`) FROM ', @VTable, ';'))); PREPARE stmt FROM @VAllCols; EXECUTE stmt; DEALLOCATE PREPARE stmt;"

最後に

自動でカラム情報を抽出しているのでカラムの順番は決められないのでご注意ください

参考サイト

2023年11月14日火曜日

M2 Mac で Audacity に FFmpeg ライブラリが設定できないときの対処方法

M2 Mac で Audacity に FFmpeg ライブラリが設定できないときの対処方法

概要

Homebrew でインストールした ffmpeg を audacity に設定しようとしても「FFmpeg ライブライが見つかりません」というエラーになって設定できませんでした
そんな場合の対処方法を紹介します

環境

  • macOS 14.0
  • Audacity 3.4.1
  • FFmpeg 6.0 (via homebrew)

対処方法

homebrew ではなく audacity から ffmpeg をインストールする

インストール方法はこちら

原因

もしかすると homebrew 版の ffmpeg がうまくインストールできていないのかもしれません
ffmpeg -v など打つと「Library not loaded」となり表示されませんでした

  • brew install lame
  • brew reinstall ffmpeg

を試して見ると audacity 側でもライブラリを読み込んでくれるかもしれません

もしくは以下の再インストール方法も役に立つかもしれません

  • brew uninstall librist --ignore-dependencies
  • brew uninstall mbedtls --ignore-dependencies
  • brew reinstall ffmpeg

最後に

ダメな場合は素直にインストーラを使って ffmpeg をインストールしましょう

2023年11月13日月曜日

(SQLAlchemy) RSA Encryption not supported - caching_sha2_password plugin was built with GnuTLS support

(SQLAlchemy) RSA Encryption not supported - caching_sha2_password plugin was built with GnuTLS support

概要

mysqld 側の設定ではなかったので対処方法を紹介します

環境

  • Ubuntu 22.04
  • MySQL 8.0.34
  • Python 3.11.3
    • SQLAlchemy 2.0.20

対応方法

ユーザを mysql_native_password で作成し直します

DROP USER "user01"@"172.22.%";
CREATE USER "user01"@"172.22.%" IDENTIFIED WITH mysql_native_password BY "xxx";
GRANT ALL PRIVILEGES ON *.* TO "user01"@"172.22.%";

2023年11月12日日曜日

mkdocs-material 超入門

mkdocs-material 超入門

概要

mkdocs-material は Python 製のドキュメントツールです
とりあえず動かすところまでやってみました

環境

  • macOS 14.0
  • Python 3.11.6
    • mkdocs-material 9.4.7

インストール

  • pipenv install mkdocs-material

サイト作成

  • pipenv run mkdocs new mkdocs_test

テーマ設定

  • vim mkdocs_test/mkdocs.yml
site_name: My Docs
theme:
  name: material

ページ追加

  • vim mkdocs_test/docs/test.md
# This is a test page
![image](https://picsum.photos/200/300)

動作確認

  • pipenv run mkdocs serve -f mkdocs_test/mkdocs.yml

最後に

とりあえずページを追加して動作するところまでやってみました
いろいろなプラグインがありブログサイトなども簡単に作ることができるようです

参考サイト

2023年11月11日土曜日

ChatGPT を API から使ってみる

ChatGPT を API から使ってみる

概要

ChatGPT を API からコールしてみました
SDK は Python のものを使っています
OpenAI のアカウントは取得済みの前提になります
なお以下の操作は有料操作になるため課金が発生するので注意しましょう

環境

  • macOS 14.0
  • Python 3.11.6
    • openai 1.1.1

API Key の取得

ここから取得しましょう

「Create new secret key」を押しキーの名称を登録すれば OK です

ライブラリインストール

  • pipenv install openai

キーのエクスポート

自動的に環境変数を拾ってくれるようなので設定します
実行時でも OK です

  • export OPENAI_API_KEY='sk-xxx'

サンプルコード

from openai import OpenAI

client = OpenAI()

completion = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        # system の content はチャット全体のコンテキストを設定します、今から質問する概要的な文を設定します
        {"role": "system", "content": "Python の fastapi について質問です。"},
        # user の content は 実際の質問を書きます
        {"role": "user", "content": "dependency の使い方を教えてください。"},
    ],
)

print(completion.choices[0].message)

実行

  • OPENAI_API_KEY=‘sk-xx’ pipenv run python app.py

429 エラーになる場合は

openai.RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}

素直に支払い方法を登録しましょう
最低でも $5 からのチャージが必要です

最後に

かなり簡単に使うことができます
汎用的な会話でいいのであればもはや自分でモデルを作ってアプリケーション化するのは不要かもしれません (お金を気にしないのであれば

参考サイト

2023年11月10日金曜日

Keras 学習コンテンツ一覧

Keras 学習コンテンツ一覧

概要

Keras の使い方を学びたかったので簡単な使い方から RNN を使った文章生成までやってみました
それぞれ記事にしたので一覧をまとめておきます
また学習した感想など記載します

環境

  • macOS 14.0
  • Python 3.11.6
    • keras 2.14.0
    • tensorflow 2.14.0
    • matplotlib 3.8.1

記事一覧

その他記事

感じたこと

  • Keras の使い方もそうだが numpy の使い方のほうが重要だと感じた
    • numpy で作成したベクトルの扱い方
    • 多次元ベクトル、shape の情報をイメージできるか
    • reshape など
  • Keras のレイヤーを理解し使えるようになるまでは大変そう
    • 入力の数と出力の数を考慮してレイヤーを結合できるか
    • 学習時と評価時でちゃんと適切なテストデータを作成することができるか
  • 学習よりもデータを生成、クリーニングする段階のほうが重要であり難しいコードになりがちな気がする
    • numpy や tf.data を使って学習に必要なフォーマットに落とせるか
    • RNN の場合は「一つずらす」必要があるのでこれをコードで表現できるか
    • numpy や tf.data は最悪使わないでもできるので無理に使わなくてもいい
  • コードをリファクタリングしたほうが理解が深まる
    • なぜかすべての紹介されているコードがすべて一枚ペラのコードばかりでクラス化されていたり冗長部分の排除ができていなかった
    • TestData, Model, Test など必要な機能、役割ごとに分割して管理すると何をやっているかもわかりやすい気がした
    • 自分が紹介してるコードはクラス化されています
  • ハイパーパラメータについて
    • こればかりは数学の知識や経験が大事になってくる
    • サンプルの値をそのまま使ってもいいがハイパーパラメータの変化による効果や仕組みを理解してないと Keras を理解したとは言えないと思う
    • それでも王道を使っておけば深層学習自体のポテンシャルの高さである程度の精度は出すことができる (それ以上を求める場合には数学の知識を含めハイパーパラメータの扱いが必要な気がする
  • 評価関数、損失関数など
    • ここも経験や数学の知識によって精度に大きく影響する
    • 一流を求めるなら自作の関数の作成と理解が必要
    • それでもデフォルトの関数を使えばそれなりの評価が出てしまう

2023年11月9日木曜日

Keras + RNN の文章生成チュートリアルをリファクタしてみた

Keras + RNN の文章生成チュートリアルをリファクタしてみた

概要

過去にやった文章生成のチュートリアルをリファクタして更に理解を深めてみました

環境

  • macOS 14.0
  • Python 3.11.6
    • keras 2.14.0
    • tensorflow 2.14.0
    • matplotlib 3.8.1

サンプルコード

import os
from dataclasses import dataclass
from typing import Optional

import numpy as np
import tensorflow as tf
from keras.src.callbacks import History
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.layers import GRU, Dense, Embedding
from tensorflow.keras.losses import sparse_categorical_crossentropy
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import get_file


# テストデータを管理するクラス
class TestData:
    # 文章を分割する長さ
    SEQ_LENGTH = 100
    # シャッフル用のバッファサイズ
    BUFFER_SIZE = 10000
    # シャッフル後の分割単位
    BATCH_SIZE = 64

    def __init__(self) -> None:
        # テキストデータのダウンロード
        self.__download()
        # 各種文字と文字インデックスの組み合わせの辞書と配列を作成
        self.char2idx = self.__gen_char2idx()
        self.idx2char = self.__gen_idx2char()
        # テキスト情報を数字情報のインデックス番号の配列に変換
        self.text_as_int = np.array([self.char2idx[c] for c in self.text])
        # TensorSliceDataset の作成、配列で要素は tf.Tensor に変換されたテキスト情報を数字情報のインデックス番号がある
        self.char_dataset = tf.data.Dataset.from_tensor_slices(self.text_as_int)
        # 100 文字ごとに分割して TensorSliceDataset を再度作成、BatchDataset になる
        # ここで説明変数と目的変数が生成される
        self.sequences = self.char_dataset.batch(
            self.SEQ_LENGTH + 1, drop_remainder=True
        )
        # 各 100 文字ごとの文章情報を1文字づらした [input: tf.Tensor, target: tf.Tensor] 配列を作ります、全文章分 map で繰り返して作ります
        dataset = self.sequences.map(self.shift)
        # シャルフルし再度 64 個ごとにまとめます、BatchDataset になる
        self.dataset = dataset.shuffle(self.BUFFER_SIZE).batch(
            self.BATCH_SIZE, drop_remainder=True
        )

    @property
    def vocab(self) -> list:
        # 文字を分割して辞書順に並び替えたリストを生成
        return sorted(set(self.text))

    def __download(self):
        path_to_file = get_file(
            "shakespeare.txt",
            "https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt",
        )
        self.text = open(path_to_file, "rb").read().decode(encoding="utf-8")

    def __gen_char2idx(self) -> dict:
        # 文字に対応した数字のインデックス番号の組み合わせを生成
        return {u: i for i, u in enumerate(self.vocab)}

    def __gen_idx2char(self) -> np.ndarray:
        # 文字を分割して辞書順に並び替えたリストを ndarray 形式に変換
        return np.array(self.vocab)

    def shift(self, chunk: tf.raw_ops.BatchDataset):
        # RNN 学習のために1文字づらしたシーケンスを作成します
        # :-1 は最後から1文字抜いた先頭からすべての文字 (説明変数)
        independent = chunk[:-1]
        # 1: は先頭から1文字抜いた最後までのすべての文字 (目的変数)
        dependent = chunk[1:]
        return independent, dependent


# RNN モデルを管理するクラス
class RNNModel:
    EMBEDDING_DIM = 256  # 埋め込み層の次元数
    RUN_UNITS = 1024  # RNN ユニット数
    model: Sequential

    def __init__(self, vocab_size: int, batch_size) -> None:
        self.model = Sequential()
        self.vocab_size = vocab_size
        self.batch_size = batch_size

    def build(self, batch_size: Optional[int] = None, new: bool = False):
        if batch_size is None:
            bs = self.batch_size
        else:
            bs = batch_size
        if new:
            self.model = Sequential()
        # 埋め込み層、固定次元の密ベクトルに変換するレイヤー
        self.model.add(
            Embedding(
                self.vocab_size,
                self.EMBEDDING_DIM,
                batch_input_shape=[bs, None],
            )
        )
        # RNN 層、今回は GRU レイヤーを使う
        self.model.add(
            GRU(
                self.RUN_UNITS,
                return_sequences=True,
                stateful=True,
                recurrent_initializer="glorot_uniform",
            )
        )
        # 出力層
        self.model.add(Dense(self.vocab_size))

    def compile(self) -> None:
        self.model.compile(optimizer="adam", loss=Loss().keras_scc)

    def show(self):
        self.model.summary()

    def train(
        self,
        dataset: tf.raw_ops.BatchDataset,
        epochs=10,
        callbacks=[],
    ) -> History:
        history = self.model.fit(dataset, epochs=epochs, callbacks=callbacks)
        return history

    def save(self, file_name="my_model"):
        self.model.save(file_name)

    def rebuild(self, checkpoint_dir: str = "./training_checkpoints"):
        # テスト時の入力のバッチサイズは1になるので batch_size=1 で再度ビルドする
        self.build(batch_size=1, new=True)
        self.model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
        self.model.build(tf.TensorShape([1, None]))


# 損失関数を管理するクラス
class Loss:
    def keras_scc(self, y_true, y_pred):
        # sparse_categorical_crossentropy をカスタム
        return sparse_categorical_crossentropy(y_true, y_pred, from_logits=True)


# モデルを再度構築するので1回目のモデルを保存するためのコールバック
class Callback:
    @classmethod
    def save_model(cls) -> ModelCheckpoint:
        checkpoint_dir = "./training_checkpoints"
        checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
        return ModelCheckpoint(filepath=checkpoint_prefix, save_weights_only=True)


# 実際にモデルを評価するためのクラス
@dataclass
class Result:
    text: str

    def show(self):
        print(self.text)


class Test:
    # 1.0 より大きいほど意外なテキストを生成する、1.0 は予測したそのままの値になる
    TEMPERRATURE = 1.0
    # 生成する文字数
    NUM_GENERATE_CHAR = 1000

    def __init__(
        self, model: RNNModel, test_data: TestData, start_string: str = "ROMEO: "
    ) -> None:
        self.model = model
        self.test_data = test_data
        self.start_string = start_string
        # start_string を数字のインデックス配列に変換
        input_eval = [self.test_data.char2idx[char] for char in start_string]
        self.input_eval = tf.expand_dims(input_eval, 0)

    def run(self) -> Result:
        self.model.model.reset_states()
        generated_text = []
        for _ in range(self.NUM_GENERATE_CHAR):
            # 文字の予測
            predictions = self.model.model(self.input_eval)
            # サイズが1の次元を削除し次元数を減らす
            predictions = tf.squeeze(predictions, 0)
            # カテゴリー分布をつかってモデルから返された文字のインデックス番号を予測
            predictions = predictions / self.TEMPERRATURE
            # 最後の要素が入力に対して予測された文字のインデックス番号
            predicted_id = tf.random.categorical(predictions, num_samples=1)[
                -1, 0
            ].numpy()
            # 過去の隠れ状態とともに予測された文字をモデルへのつぎの入力として渡す
            self.input_eval = tf.expand_dims([predicted_id], 0)
            # インデックス番号から文字への変換と結果の格納
            generated_text.append(self.test_data.idx2char[predicted_id])
        return Result(text=self.start_string + "".join(generated_text))


if __name__ == "__main__":
    # テストデータ作成
    test_data = TestData()
    # モデル生成
    model = RNNModel(len(test_data.vocab), TestData.BATCH_SIZE)
    model.build()
    model.compile()
    # # モデルの訓練
    model.train(test_data.dataset, callbacks=[Callback.save_model()])
    # モデルの評価
    model.rebuild()
    model.show()
    test = Test(model, test_data)
    result = test.run()
    # 結果を表示
    result.show()

ちょっと解説

テストデータは単純なテキストを使います
テキスト情報を1文字ずつ分解し数字とのマッピング情報を作成しテキストをすべて数値化することで学習データを作成します
文字情報の数値化は自然言語ではよくある手法になります

データをシャッフルしたりバッチサイズで分割するのは Keras で学習させる際のフォーマットに変換していると理解していますがこの辺りの手法を自然に使いこなせるレベルにならないとダメかなと思います

モデルの生成は Embedding -> GRU -> Dense とシンプルです
Embedding は自然言語の学習ではほぼ必須です
GRU (Gated Recurrent Unit) は RNN の一種です
モデル学習時にチェックポイントを保存しています
評価する際に入力をシンプルにしたいので学習時のバッチサイズ64を再度チェックポイントからモデルをビルドしてバッチサイズを1にするためです

予測した値はそのまま使わずカテゴリー分布という手法を使って再度計算させています (正直理由は不明です

リファクタリング不足点

  • TestData をもう少しリファクタリングしたほうがいい
    • データの生成を init ではなくそれぞれの関数にしたほうがいい
    • 説明変数と目的変数を別のクラスで管理した方がいい
  • Model の model の管理をリファクタリングしたほうがいい
    • build で新規 or 既存の書き換えを制御するよりかは ModelFactory を作って model を個別に生成できるような仕組みにするといいかも
  • Test を柔軟にしたほうがいい
    • いろいろなデータでテストできるようにしたほうがいい
    • 生成文字列サイズなども可変にできるといい
    • 何をやっているかわからないところがあるのでコメントで補完したい

最後に

Keras を使った学習方法や流れはだいたい把握できましたがフルスクラッチでゼロから numpy や keras を使ってモデルを作るにはまだまだ学習が足りないと感じました

numpy で生成したベクトルの扱い方や keras のレイヤーの生成方法やレイヤーに対する入出力の制御、最適化あたりを駆使できるようにならないとオリジナルなモデルを作るのは難しいなと感じました

参考サイト