yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

ECS接続用のシェルスクリプトを書いた

ECSで動かしてるFageteにECS Execで入りたい場面がちょくちょくあるんだけど毎回クラスタ名とタスクID、コンテナ名を貼り付けてコマンドを作るのが面倒だった

aws ecs execute-command \
    --cluster <クラスタ名> \
    --task <タスクID> \
    --container <コンテナ名>\
    --interactive \
    --command "/bin/bash"

例えばRailsのサービスを2つ運用していてそれぞれのサービスの構成が

  • クラスタ4つ(production環境、development環境、demo環境、test環境)
  • クラスタ1つの中にサービス2つ(アプリケーション本体とバッチ用サービス)
  • さらにアプリケーション本体のタスクにはRailsのコンテナとNginxのコンテナが入っている。。

みたいな状況だと結構面倒。タスクIDは管理画面に確認しにいくか別途コマンド叩いて調べないといけないし。

なのでシェルスクリプトで接続先のクラスタ、サービス、コンテナを選択できるシェルスクリプトを作ってみた。

aws-cliコマンドでクラスタ、サービス、タスクID、コンテナの情報を引っ張ってきて後はjqやawssedで必要な部分地道に切り取ってく感じ

#!/bin/bash
echo ECSコンテナに接続します。クラスタ、サービス、コンテナ名を選択してください。
echo
echo ? クラスタを選択してください
echo
CLUSTER_NAMES=`aws ecs list-clusters --output text \
              | awk -F'/' '{print $2}'`
select CLUSTER_NAME in $CLUSTER_NAMES exit
do
  if [ "$CLUSTER_NAME" = "exit" ]; then
    echo 処理を中断します
      exit 0
  fi
    break
done
echo
echo $CLUSTER_NAMEが選択されました
echo
  echo ? コンテナを選択してください
fi
echo
SERVICE_NAMES=`aws ecs list-services --cluster $CLUSTER_NAME --output text \
              | awk -F'/' '{print $3}'`
select SERVICE_NAME in $SERVICE_NAMES exit
do
  if [ "$SERVICE_NAME" = "exit" ]; then
      echo 処理を中断します
      exit 0
  fi
    break
done
echo
echo $SERVICE_NAMEが選択されました
echo
TASKID=`aws ecs list-tasks \
    --cluster $CLUSTER_NAME \
    --service-name $SERVICE_NAME --output text \
    | awk -F'/' '{print $3}' | head -1`

CONTAINER_NAMES=`aws ecs describe-tasks --cluster $CLUSTER_NAME \
                  --tasks $TASKID \
                  | jq '.tasks[0].containers[].name' \
                  | sed 's/"//g'`

echo ? コンテナを選択してください

echo
select CONTAINER_NAME in $CONTAINER_NAMES exit
do
  if [ "$CONTAINER_NAME" = "exit" ]; then
      echo 処理を中断します
      exit 0
  fi
    break
done

echo
echo $CONTAINER_NAMEが選択されました
echo $CONTAINER_NAMEに接続します
echo

if [ `echo $CONTAINER_NAME | grep 'nginx'` ] ; then
  SHELL_PATH="/bin/sh"
elif [ `echo $CONTAINER_NAME | grep 'demoa_service'` ] ; then
  SHELL_PATH="/bin/ash"
elif [ `echo $CONTAINER_NAME | grep 'demob_service'` ] ; then
  SHELL_PATH="/bin/bash"
else
  echo "error unexpected container"
  exit 1
fi

aws ecs execute-command \
    --cluster $CLUSTER_NAME \
    --task $TASKID \
    --container $CONTAINER_NAME \
    --interactive \
    --command $SHELL_PATH

こんな感じで動く。実行サーバはaws-cliのver2が入っているEC2でbashで呼び出す想定。

bash ecs_connection.sh
ECSコンテナに接続します。クラスタ、サービス、コンテナ名を選択してください。

? クラスタを選択してください

1) demoa-service-production    3) demoa-service-development     5) demoa-service-development
2) demoa-service-demo        4) demoa-service-production  6) exit
#? 3

demoa-service-developmentが選択されました

? コンテナを選択してください

1) demoa-service-development-app  3) exit
2) demoa-service-development-batch
#? 2

demoa-service-development-appが選択されました

? コンテナを選択してください

1) demoa_service_development_app
2) demoa_service_development_nginx
3) exit
#? 2

demoa_service_development_appが選択されました
demoa_service_development_appに接続します

ゾンビ系FPSを作成するオンライン講座を受講してみた

UdemyでUnityを使ってバイオハザードみたいなゾンビ系FPSを作成する講座を受講してみた。

www.udemy.com

ノンフィールドRPGに続けてUnityの講座を受講するのは2つ目。

https://ikeyu0806.hatenablog.com/entry/2021/10/24/123335

できたもの

こんなの

vimeo.com

今回もセールで買ったんで1400円くらいで7時間の講座の割には結構それっぽいものできたし学びにもなって満足。

学んだこと

  • 3Dモデルの動かし方、アニメーション実装
  • アニメーションエディタの使い方
  • FPSゲームの視点の実装
  • ゾンビの行動の実装方法(歩く、プレイヤーを追いかけて走る、攻撃、倒れる)
  • フィールド上のゾンビの自動生成(スポーン)
  • 人型の3Dモデルに銃を持たせて発砲⇨弾のゾンビへの衝突判定実装
  • ダメージ、弾薬の管理と画面表示
  • HP、弾薬回復アイテムの実装
  • ゲームオーバ実装
  • ゲームクリア実装
  • 地形の作り方
  • SE(Sound Effect)とBGMの導入
  • 自分のHPによって視界を暗くしていく方法(ポストプロセス)
  • タイトル画面からの遷移

使った素材

3Dモデルは「Mixamo」、地形のテクスチャは「TextureHaven」、サウンドは「SONNISS」というサイトで無料配布されている素材を使った。

Mixamo

Textures • Poly Haven

GDC Game Audio Bundles - SONNISS - Royalty Free Sound Effects

3Dゲーム開発の知見

今回初めて3Dゲームの開発体験してみたけど

  • 3Dモデルのアニメーション
  • 地形の生成

に関しては自分で書いたプログラムでどうこうするUnityエディタ上の操作でほとんと作れるっぽい

地形

地形はUnityエディタ上で「Terrain」という3Dオブジェクトを作ってそこから山や木を生やしたり地形の表面にテクスチャ貼ったりをフォトショでブラシを塗るような感覚で作っていける。ちょっとビビるくらい簡単で便利。

3Dオブジェクトの操作

3Dオブジェクトに関しては「Mixamo」からダウンロードした時点で大分ゲームに組み込むための仕組みができていてそこがどう作られたのかは謎。「Blender」みたいな3DCG制作のソフトを使って自分で何かしら作ってみないと理解できないかも

MixamoからダウンロードしてUnityにインポートした時点でアニメーションが既に入ってる。例えばプレイヤーの3Dオブジェクトなら弾の装填とか銃を構えるアニメーション、ゾンビなら攻撃したり走ったり倒れたりする時のアニメーションが既に用意されていて、例えばフィールド上でゾンビを動かそうと思ったらUnity開発者は以下の挙動をプログラムすればいい

  • ゾンビのX、Z座標を一定の速度で動かし続ける。この座標が動いている間はゾンビが歩くアニメーションを繰り返すようにする
  • プレイヤーとの距離が一定に縮まったら歩くアニメーションから走るアニメーションに切り替えて移動速度を速くする
  • さらにプレイヤーとの縮まったら攻撃アニメーションに切り替えてプレイヤーのHPを減らす

アニメーション自体も3Dモデルの足や腕をプログラムで操作するのかな。。と思ってたけど少なくとも今回はそうではない

残ったバグ

最後まで動画見て同じようにUnityエディタ操作してC#スクリプト写経したはずが何故かバグが残ってしまった。

  • アイテムが何故か取れない
  • フィールド上の木の幹が謎に紫色になっている

この辺りは後で直したいが。。別のUnity講座もう買ってあるしそっちやりたいんで一旦放置

ノンフィールドRPGを作成するオンライン講座を受講してみた

Unityでゲーム作ってみたいと思い、Unityを使ってノンフィールドRPGを作成するUdemyの講座を受講してみた。 www.udemy.com

Unity選んだ経緯

2021年現在ゲームエンジンの人気はUnityかUnreal Engineの2択っぽい。以下Unreal Engineと比較しての選定理由

  • Unreal EngineはゲーミングPCみたいな高スペックのPCじゃないと開発が辛そう。普段使いのPCだとUnityの方が良さそう
  • Unreal EngineはPS5とかXBoxみたいな据え置きゲームの開発に使われていそうだが、とりあえずスマホゲームやブラウザゲームが作りたい。であればUnityの方が手軽そう。
  • Unreal Engineはプログラミングをしなくてもゲームが作れるのに対してUnityはC#スクリプトを書いて開発していく。ただ、ゲーム開発は未経験だけど職業プログラマだしC#書くのは問題なさそう
  • ネットの記事や書籍、オンライン講座などの情報も現状Unityの方がUnreal Engineより多そう

というような理由でとりあえずUnityに手を出してみることにした。

できたもの

講座内ではAndroidアプリとして公開すること前提に進めるけど、build設定をWebGLにしてブラウザで動かしてみた

vimeo.com

できるようになったこと

  • Unityの基本的なGUI操作
  • C#での基礎的なプログラミング
  • C#スクリプトでの2Dでのゲームオブジェクトの操作方法(ボタンクリックからのテキスト、画像、サウンドの切り替えやシーンの遷移など)
  • Prefabでのオブジェクトの再利用
  • サウンドの設定
  • アセットストアの使い方
  • 画像や音声などの素材の使い方
  • アニメーションのやエフェクトの実装

エラーメモ

初期設定

Mac Book ProでOSはBig Surの環境で構築してみた。

Unity HubというUnityの複数バージョン管理するツールでUnity2020をインストールしてみる。

unity3d.com

で、プロジェクト選択して立ち上げたら「「The project you are opening contains compilation errors. ...」みたいなエラーが出て言われるがままSafe Modeで立ち上げたらコンソールにエラーマークが出てるけど具体的なメッセージが出てこない。。

Safe Mode無視してそのまま立ち上げたら普通に動いたんだけどなんか気持ち悪いな。。

とりあえずエラーが出たのは初回だけでその後は普通に動作してそう。

DOTWeen

Udemyで買った講座を参考にDOTWeenというオブジェクトにアニメーションを追加するためのライブラリをインストールしたらコンパイルエラーになったがPCごと再起動したら直った。Unityだけ再起動してもダメだったんだけどこれもなんだったんだろう。Dockerコンテナいくつも立ち上げっぱなしにしてたりもしたんでリソース不足だったのか?

その他

WebGL向けの対応

この講座はAndrioidを想定して作られているけど自分としてはWebGLで自作のブラウザゲームを作って作って公開するのが第一目標なのでbuild設定をWebGLにして受講してみた。ただその場合は以下2点講座の内容と別に対応が必要。

  • 画面幅がそのままだと崩れるので各シーンのCanvasの「UI Scale Mode」を「Scale With Screen Size」に変更する必要あり
  • 日本語を表示する際にデフォルトのArelだと表示できないので日本語フォントをインストールする必要あり

参考 naburacortan.blogspot.com qiita.com

今後

とりあえずクイズゲームみたいなシーンとテキスト、画像の遷移と最適源のエフェクトやアニメーションのみで作るゲームは自作できそうだけどもう少し凝った2Dゲーム(フィールド付きRPGとかレースゲームとか格闘ゲーム)を作るにはもう少しインプットの時間必要かな。。

あとUnityは3Dゲームも作れるけどそっちは全然わかってないので別途調べてみたい。

Railsデプロイ時の認証周りの対応メモ

最近新規で作ったRailsサービスのデプロイ作業してたら認証周りの対応が地味に面倒だったので対応メモ。

前提

  • JWTで認証するREST APIを提供するRailsサービス
  • Railsのバージョンは6.1.4
  • AWS ECS上のコンテナで稼働
  • RailsはPumaで起動してNginxとsocket通信させている

ロードバランサのヘルスチェック対応

まずデプロイしたらロードバランサ(ALB)のヘルスチェックで落ちるようになった。これは心当たりがあってrailsのconfigが原因で例えばrspecを起動する時にapplication.rbかtest.rbに以下の指定をしないと落ちる

config.hosts << "www.example.com"

ので本番環境もconfig.hostsの指定をしてないせいで、ロードバランサからのリクエストを拒否してしまってたのが原因とは気づいたけど、これどうやって許可するのかで迷った。

とりあえずconfig.host.clearと設定してしまえば通るんだけどなんか違う気がする。。

で、調べたらNginxでヘッダに許可するhostを指定する方法があってこれが良さそうだったのでこの通りに対応して通した。

Rails6のActionDispatch::HostAuthorizationとELBのヘルスチェックの共存 - Qiita

Railsのログ出力設定ではまる

これでヘルスチェックが通ってECSが動いたんだけど、その後適当なJSON投げて動作確認しようとしたら401が返ってきて通らない。CloudwatchはRailsとNginxでそれぞれロググループを作っていたんだけど、Nginx側のロググループにのみ401エラーが出ていてRails側には出力が何もない。

ってことはNginxの認証で落ちていてRailsに届いてないのでは??と思ってNginxの設定ファイルを見返したけどどう見ても問題なさそう。

それで試しにRailsのJWTの認証外してみたら通った。結局JWTがHTTPリクエストのAuthorizationのHeaderの鍵が間違っていたのが原因だったらしい。

じゃなんでRailsのログがCloudwatchに出てないんだ??と思って調べてたらconfigの設定でログが標準出力に出ていなかったのが原因だった。

config/environments/production.rb

  if ENV["RAILS_LOG_TO_STDOUT"].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)
  end

環境変数RAILS_LOG_TO_STDOUTを追加したらCloudwatchで確認できるようになって解決。

<input type="file" accept="image/*">タグでAndroidの写真を扱うとエラーになる場合がある

HTMLで画像のみ受け付けるフォームを以下のように作って検証していたらAndroid端末でエラーが出るようになった。

<input type="file" accept="image/*">

スマホ端末だと上記のタグをつけた場合に保存されている写真をアップロードするのではなくカメラアプリを起動させて撮影した写真をそのまま送信できるんだけど、そうするとHuaweiAndroidでエラーが発生した。

で、調べたら対象の写真がHEIC形式(HEIF形式)でHTMLの"accept=image"がHEIC形式に対応していないのでエラーになっていたっぽい。

iPhoneで撮影した写真もHEICだけどiPhoneの場合はinputタグからアップロードする場合JPEGに勝手に変換してくれているらしい。自分はiPhoneユーザなのでここで差分があって気づかんかった。

対応

今回は上記のフォームから写真をバックエンドにpostしてからS3に送信したかったんだけど機能要件的にaccept="image"を外してサーバサイドでバリデーションかければいいよね。というかよく考えたら写真以外も扱える機能にしてもいいじゃん。という話になったけど、これ気づきにくいな。検証者がiPhoneユーザしかいないと辛い

「ゼロから作るDeep Learning」を読み返した

一度ざっと読んだものの理解できず本棚に放り込んだままになってたオライリーの「ゼロから作るDeep Learning」を読み返した。

文系卒で微分や行列計算などは習っていないけどその知識は必要そうなので高校生向けの本をざっと読んでおいた。

数学知識は付け焼き刃なんでまだ理解できてないところも正直あるんだけど、勉強がてら全体の流れをざっくり整理して書いてみる。

内容

なるべくライブラリを使わずディープラーニングで画像分類器を作るという内容。

同じく機械学習の本でこの間読んだ「自然言語処理アプリケーション開発入門」だとKerasというGoogleが作ったTensorflowという機械学習のラッパーライブラリを使ってディープラーニングを実装していたので実践的ではあったもののブラックボックスだった部分は多かった。この本はその部分の理解を助けてくれるような内容。

画像の扱いについて

ディープラーニングの実行には入力データを数字の配列(多次元配列)として渡さないといけない。

自然言語処理の場合は単語を辞書に登録したり出現頻度調べたりベクトル化のために手間がかかったけど今回扱う画像データの場合はそもそも縦、横、チャンネルの多次元配列の構造になっているのでその点はあまり気にする必要がない。

パーセプトロン

まず基本となるのがパーセプトロンという入力値に重みをかけて0か1の数字を出力するアルゴリズム

この本ではパーセプトロンを使ってAND、OR、XORなど2進数計算からなる論理回路の実装が説明される。

パーセプトロンでのAND回路実装例

def AND(x1, x2):
  w1, w2, theta = 0.5, 0.5, 0.7
  tmp = x2*w1 + x2*w2
  if tmp <= theta:
    return 0
  elif tmp > theta:
    return 1

これにバイアスを足して重みを変えるとORやNAND回路も作れる。

XOR(入力が[0, 0]か[1, 1]の時に0を出力してそれ以外は1を出力する回路)の場合はANDのように単純に実装はできないけど、パーセプトロンを多層にすることで実装ができる。

def XOR(x1, x2):
  s1 = NAND(x1, x2)
  s2 = OR(x1, x2)
  y = AND(x1, x2)
  return y

このように層を複数重ねたパーセプトロンを多層パーセプトロンという。多層パーセプトロンを使うと複雑な問題も解決でき理論上コンピュータそのものも作ることができる。

ニューラルネットワーク

パーセプトロンは複雑な問題も解決できるが、重みを人の手で設定しなければならない。ニューラルネットワークではこの重みを設定する作業をデータから自動で学習させることができる。(パーセプトロンニューラルネットワークの定義は記事や本によってまちまちな気もする)

活性化関数

活性化関数とは「閾値を境として出力が切り替わる関数」のことで「パーセプトロンの場合はステップ関数を実行している」と説明できる。

ステップ関数

ステップ関数の単純な実装

def step_function(x):
  if x > 0:
    return 1
  else:
    return 0

シグモイド関数

ニューラルネットワークではこの活性化関数にステップ関数以外の関数を使う。パーセプトロンニューラルネットワークの違いはこの活性化関数のみ。ニューラルネットワークでよく使われる活性化関数の一つにシグモイド関数がある。

numpy(np)を使った実装例

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

exp関数は例えばexp(1)とすると2.71828183、exp(2)とすると7.3890561を返す。

ネイピア数と指数関数 │ Python 数値計算入門

ステップ関数とシグモイド関数の入力値をx軸、出力値をy軸としてグラフで描画するとステップ関数は0を境に急速に出力が変わるのに対してシグモイド関数は滑らかな曲線を描く。

また、ステップ関数は0か1を出力するのに対してシグモイド関数は「0.731...」、「0.881...」などの実数を返す。

ReLU関数

シグモイド関数ニューラルネットワークの歴史で古くから使われてきたが、最近ではReLU関数が使われることが多い。 ReLU関数は入力値が0以上ならそれをそのまま出力し0以下なら0を出力する関数

def relu(x):
  return np.maximum(0, x)

シグモイド関数を使ったニューラルネットワークの実装例

# indentity_functionは仮置き
def indentity_function(x):
  return x

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

def init_network:
  network = {}
  network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
  network['b1'] = np.array([[0.1, 0.2, 0.3])
  network['W2'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
  network['b2'] = np.array([0.1, 0.2])
  network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
  network['W3'] = np.array([0.1, 0.2])
  
  return network

def foword(network, x):
  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['b1'], network['b2'], network['b3']

  a1 = np.dot(x, W1) + b1
  z1 = sigmoid(a1)
  a2 = np.dot(z1, W2) + b2
  z2= sigmoid(a2)
  a3 = np.dot(z2, W3) + b3
  y = identity_function(a3)

  return y

network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)

init_networkで重みを初期化。その後「np.dot」でnumpyを使って行列計算を実行し入力値に対して重みとバイアスをかけていく。 その結果を活性化関数(この場合はシグモイド関数)に与えて重みを調整する。

それを繰り返して最適な重みを出力する。

indentity_functionは出力層の活性化関数でこの例では入力をそのまま出力するのみ。出力層の活性化関数については後述。

出力層の設計

入力値に重みとバイアスをかけて結果を出力し、その結果を活性化関数が受け取って重みを何層かに渡って調整する。その後、結果をシステムの目的に応じて選択された関数に与えて出力する必要がある。

上記のindentity_functionのような入力をそのまま返す関数は恒等関数と呼ばれる

ニューラルネットワークはも回帰問題にも分類問題にも使うことができ、回帰問題の場合は恒等関数。分類問題の場合はソフトマックス関数が使われる。

回帰問題とは連続値などの値の予測のことで売り上げ、株価予測のシステムで採用されたりする。

今回の画像識別は分類問題になるのでソフトマックス関数を実装する。

ソフトマックス関数

def softmax(a):
  exp_a = np.exp(a)
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a
  
  return y

softmax関数の使用例

>>> a = np.array([0.3, 2.9, 4.0])
>>> y = softmax(a)
>>> print(y)
[0.01821127, 0.24519181, 0.73659691]
>> np.sum(y)
1.0

このようにソフトマックス関数は0から1の間の実数値を出力する。このおかげでソフトマックス関数の出力値は「確率」として解釈できる。

この例では3番目のクラスである確率が最も高く約73%なので3番目のクラスに分類される。など

ニューラルネットワークの学習

上記の処理で重みを調整することができるが、その重みが正しいか判定するために損失関数を使う。

ニューラルネットワークの学習では、ある「ひとつの指標」を手がかりにパラメータを探索する。ニューラルネットワークの学習で用いられる指標は「損失関数」と呼ばれる。

この損失関数は、任意の関数を用いることができるが、一般には二乗和誤差や交差エントロピー誤差などが用いられる。どっちも正解データとニューネットワークの出力結果の誤差を判定できる関数

機械学習の問題は、「訓練データに対する損失関数を求め、その値をできるだけ小さくするようなパラメータを探し出す」ということ。

なぜ認識精度ではなく損失関数を指標にするのかというと、パラメータの微分がほとんどの場所で0になってしまい、パラメータの更新ができなくなってしまうから。

重みのパラメータを少しだけ変化させたときに正解にどれだけ近づいているのかを判定したいので、ある瞬間の変化の量を表す数値微分を使いたいが、そのためには損失関数を指標にする必要がある。

誤差逆伝播

重みパラメータに対する勾配を数値微分ではなく「誤差逆伝播法」という手法で求めると計算の効率が良くなる。で、この誤差逆伝播法について1章分割かれてるけど正直わからん。多分微分についてちゃんと理解してリベンジしないと無理そうなので誤差逆伝播法の詳細については略。

CNN

画像認識や音声認識など至る所で使われる畳み込みニューラルネットワーク(CNN)というニューラルネットワークのモデルがある。 これまで見てきたニューラルネットワークと同様でレゴブロックのようにレイヤを組み合わせて作ることができるが、新たに「Convolutionレイヤ(畳み込み層)」と「Poolingレイヤ(プーリング層)」が登場する。

これまで見てきたニューラルネットワークでは全結合層(Afflineレイヤー)を用いていた。全結合層はデータの形状が無視されてしまう問題がある。入力データが画像の場合は縦、横、チャンネル方向の3次元の形状だが、全結合層に入力するときには3次元のデータを平らな1次元にする必要がある。

空間的に近いピクセルは似たような値であったり、RGBの各チャンネル間にはそれぞれ密接な関わりがあったり、距離の離れたピクセル同士はあまり関わりがなかったりなど3次元の形状には汲み取るべき本質的なパターンがあるが、全結合層ではそれが無視されてしまう。

一方、畳み込み層(Convolutionレイヤ)は形状を維持することができる。

その後のプーリング層は縦横方向の空間を小さくする。識別に不必要な特徴量をそぎ落として識別に必要な特徴量を抽出する。

その他

GPUとかディープラーニングの歴史、応用事例の話題もあったけどこの記事書いたのはディープラーニングを使って画像分類器を実装する方法を整理したかったからなんでその辺りは略

終わりに

活性化関数や損失関数についてはこういったユースケースではこの関数を使うとうまい結果が得られるよ。。という知識は得られたけどどうしてその関数だとうまくいくのか理解がぼんやりしている気はする。数式とコードが詳しく記載されてるんだけどちょくちょく読めない数学記号とかがあって辛い。この辺り理解するにはやっぱり数学ちゃんとやった後にまた読み返さないとダメそう。

数字ベクトルを入力⇨重みとベクトルをかける⇨活性化関数で出力を調整する⇨損失関数で重みを調整して最適な重みを導く。。という仕組みだったり精度を高めるための手法にどんなものがあるのかざっくり知れたんでとりあえずいいや

「自然言語処理アプリケーション開発入門」を読んだ

普通のバックエンドエンジニアでも機械学習に関わる機会は割とありそうだしちょっと調べとくか。。

と思って機械学習周りの本いろいろ読んでたんだけど、今の自分の前提知識だと「自然言語処理アプリケーション開発入門」がちょうどいい感じだった。

https://www.amazon.co.jp/dp/B07YP84B1R?btkr=1

読んだ段階での自分のスキル

  • プログラム実装は毎日やってるが実際の仕事での機械学習自然言語処理を使った機能実装は未経験
  • 文系卒ということもあり機械学習分野で使われる微分、確率統計、線形代数などの数学知識が薄弱。一応独学で大学数学の入門書をざっと読んだ程度

どんな人向きか

これより先に読んだ「ゼロから作るdeep-learning」だとライブラリを使わずにディープラーニングを実装する。っていう本でちゃんと理論を理解するには良書なんだけどそれに比べて「自然言語処理アプリケーション開発入門」は大分実践的な印象。理論よりも手を動かして作るのが好きなエンジニアにはウケのいい内容だと思う。

数式を使ったアルゴリズムの説明は出てこず、sklearnやkerasといった機械学習ライブラリを使ってとにかくプログラムを作っていく。なので機械学習の詳細な実装を知りたければ他の本を読まないといけないけど、その代わり精度を上げるための手法だったり具体的なデータの集め方など実際の現場で使えそうな内容が多かった。

内容

最初から最後まで基本的には自然言語処理の手法を使って対話エージェントを作る内容。最初は理解しやすい手法から始まって少しずつ精度を上げる手法が紹介され最終的にニューラルネットワークを使った手法にして。。といった具合に徐々に難易度を上げていくような進め方をする。

以下復習がてら本書での対話エージェントの作りをざっくり書いてみる。

対話エージェントの仕組み

質問をクラスに分類する

まず、似たような質問を「クラス」としてまとめる。例えば

自然言語処理をしたいんだけど、どうすればいい?」や「日本語の自然言語処理について知りたい」

といった質問は「自然言語処理の学習について聞く質問」として「クラス0」とする

「好きなプログラミング言語は?」や「どの言語を使うのがいいかな?」

は「おすすめのプログラミング言語を聞く質問」として「クラス1」とする

で、それぞれのクラスに対する回答を用意する。例えばクラス1の「好きなプログラミング言語は?」に対しては「Pythonがおすすめです」など。

「ただこの文章入力に対してはこの回答をします」といったルールを愚直に登録するやり方には無理があるので入力される文の傾向をシステムに学習させて「どのような質問文がきたらどの回答文を返せばよいか」のパターンを取得していく。

つまり「文を入力すると、その文が属するクラスを予測し、クラスIDを出力するシステム」を作れば対話エージェントが成立する。

以下文が所属するクラスを予測する具体的な手法

まず入力された文書を「ある決まった個数の数字のまとまり」に変換する必要がある。「ある決まった個数の数字のまとまり」を以下「ベクトル」と表記する。

ベクトルの作り方

文章をわかちがきして単語に分ける→Bag of Wordsという手法で単語の出現回数をカウントする

Bag of Wordsはまず単語を辞書に登録して登録された単語ごとの出現数をカウントする特徴抽出の手法

辞書の例

単語 番号
0
あなた 1
ラーメン 2
好き 3
こと 4
5
6
7
8
です 9

ベクトルへの変換例

# 私 / は / 私 / の / こと / が / 好き/ な / あなた / が / 好き / です
[2, 1, 0, 2, 1, 2, 1, 1, 1, 1]

# 私はラーメンが好きです
[1, 0, 1, 1, 0, 1, 1, 0, 0, 1]

この本ではわかちがきの実装にはmecabという形態素解析のライブラリを使った実装が記載されていて、BoWはスクラッチでの実装とscikit-learn を使った実装が記載されている。

SVMを使った識別器

次に上記で得られたベクトルを使って識別器を作る。特徴ベクトルを入力し、クラスIDを出力することを「識別」と呼びそれを行うオブジェクトや手法を「識別器」という。

まずこの本ではSVMsupport vector machine)という手法で識別器の実装を進めていくけどSVMについての詳細説明は省略されていてscikit-learn(ライブラリ)を使ったSVMの使い方が説明されている。

まあ、とにかくベクトル作ってscikit-learnのプログラムに与えると文章に対する機械学習でのクラスIDの予測ができる。

チューニング

TF-IDF

ここまでではBoWで特徴ベクトルを計算していたけど、BoWの代わりにTF-IDFを使うことで精度を向上させることができる。 TF-IDFは「重要な単語の寄与を大きく、重要でない単語の寄与を小さくする」ことでBoWよりも文の特徴をよく表現できる。

例えば「私はラーメンもお好み焼きも好きです」、「私はラーメンが好きだ」、「私はサッカーも野球も好きだよ」という文章があったとしてこの文章を特徴づけているのは「ラーメン」「お好み焼き」「サッカー」「野球」といった「私の嗜好」を表す単語だが、BoWでは「私」や「好き」といった他の文章との比較において大して意味のない単語と同等に扱われてしまう。その問題をTF-IDFでは解決できる。

チューニングその他

文章の表記揺れや文字コードのズレを前処理で吸収する方法だったり、わかちがき部分のその他の手法だったり、SVM以外の識別器(決定木アンサンブル、k近傍方)の紹介だったりと分類の精度を高める方法が紹介されるけどそれ全て書くのはきついので略。

ニューラルネットワーク

内容の後半辺りからニューラルネットワークの説明とそれを使った識別器の実装についての説明に入る。

ニューラルネットワーク」っていうのは世間で「AI」、「人工知能」という言葉を聞いた時によくイメージされる脳細胞を模して設計された計算モデルのこと。その内容を数学的なモデルに落とし込んだものを「パーセプトロン」という。

n個の入力とそれぞれに対応した重み、固定値の入力バイアスからなる。

こんなやつ(wikipedia)↓

ja.wikipedia.org

1つのパーセプトロンは数値を1つ出力するので複数並べると全体として1つのベクトルを出力する。このまとまりを「レイヤー」という。

このレイヤーを増やして高い精度を出せるようにした手法が「ディープラーニング」と呼ばれる。

で、このニューラルネットワークを識別器として使うことができる

追記

本書ではニューラルネットワークパーセプトロンについて上記のように書いてあったけど、他の本や記事ではノードの活性化関数にステップ関数を使ったのがパーセプトロンで、シグモイド関数のような滑らかな活性化関数を使ったのがニューラルネットワークと呼ばれてたりする

ニューラルネットワークを使った識別器

単純パーセプトロンを使ったクラス分類

以下単純パーセプトロンを使って2クラスに分類する具体的なイメージ(自分なりの解釈でベクトルや重みの値はサンプルコードから転用した適当な値)

まずTF-IDFを使って入力文書をベクトル化して入力し、その入力に対して重みとバイアスをかけてを実行すると0から1の間の数字が出力される。例えば「好きなプログラミング言語は?」、「自然言語処理をしたいんだけど、どうすればいい?」みたいな質問文を用意してベクトル化して[0.2, 0.3, -0.1]、[-0.2, -0.1, 0.9]という結果が出たとする。これをパーセプトロンの入力値とする。

で、重みを[-1.64, -0.98, 1.31]、バイアスを「-0.0.5」だとすると入力ベクトルに対して 0.2 * -1.64、 0.3 * -0.98‥‥といった風にそれぞれの値に重みをかけた後にバイアスの-0.05を足した値が出力される。

で、その出力が例えば「0.3093841557917043」と「0.8256347143825868」だとする。そして数字が0に近ければクラス0、数字が1に近ければクラス1といった風にクラス分けして識別器として使うことができる。

層を深くする場合、他クラス分類の場合

上記の例だと2クラスのどちらかにしか分類できないので実用できない。他クラスの場合は出力値をクラスIDの数だけ出力して最も1に近いクラスに分類する方法が紹介されている。例えば出力値が[0.33898331, 0.65419141, 0.498738291]だとすると2つめが最も1に近いので2つ目のクラスに分類する。

また、ニューラルネットワークの層を深くして流行りのディープラーニング化すると精度の高いシステムができるけどただ層を深くするだけではダメで実現のためには様々な問題を乗り越えなければいけないんだけど、この記事は全体の流れをざっくり整理したい。。と思って書いたんでその辺りの地道なチューニングの話は略。