まるっとワーク

データ分析・電子工作など気になることを残していきます

ChatGPTを使った自動レシピジェネレーターの紹介(ChatGPT API + AWS + LINE)

ChatGPTを使った自動レシピジェネレーター サービスの作成方法を紹介します。
このサービスは、食材を入力するだけでレシピを考案し、提供するサービスです。システムの構成には、ChatGPT API + AWSサービス(Lambda + API Gateway)とLINEを使用し、連携させて、構築しています。

正直言うと、AWSサービスとLINEの連携さえ済めば、ChatGPT APIを使ってメッセージを返して貰う所だけを追加するだけなので、そこまでハードルは高くない。
それ以上に重要なことは、望んだ答えを出すような命令方法(プロンプトの設計)とアウトプットのさせ方だなと感じた。

以下先人の皆様の情報を利用して作成しており、尊敬を込めてご紹介します。
[ChatGPT]OpenAI APIでGPT-3.5系のモデル「gpt-3.5-turbo」と「text-davinci-003」をLambdaで試してみた | DevelopersIO
[ChatGPT API][AWSサーバーレス]ChatGPT APIであなたとの会話・文脈を覚えてくれるLINEボットを作る方法まとめ | DevelopersIO

目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
・LINE (developers)
・ChatGPT API

開発手順

  1. LINE Message APIを使うための環境を整える/AWS の環境を整える+LINEと連携する[省略:別ページで紹介]
  2. AWS Lambdaでコード実行(動作確認)

実装

LINE Message APIを使うための環境を整える/AWS の環境を整える+LINEと連携する[省略:別ページで紹介]

これらの手順は過去のブログで説明をしているので、そちらをご参照ください。
AWS Lambda環境では、openaiモジュールを使用するので、AWS Lambdaのレイヤーにはopenaiモジュールを入れる必要があります。
dango-study.hatenablog.jp

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
openai.ChatCompletion.createの戻り値は、class 'openai.openai_object.OpenAIObject'であり、戻り値の読み込みには注意が必要。
以下の通り対応することで、辞書形式に変更ができ、扱いやすくなる。

dictionary = openai_object.__dict__#辞書形式に変更


コード詳細↓

import json
import urllib.request
import os
import openai 
from linebot import LineBotApi
from linebot.models import TextSendMessage
from datetime import datetime 

#######APIキーを取得
openai.api_key = os.environ["Openai_ACCESS_TOKEN"] #自身で設定した環境変数の値に変更
# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN'] #自身で設定した環境変数の値に変更
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

########説明
#LINEで入力されたテキストは ['events’][0]['message’]['text’] に入っています。
#また、受信したメッセージに対してリプライを行うには、replyTokenの値を使用します。

# ログ出力関数
def logging(errorLv, lambdaName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
    print(loggingDateStr + " " + lambdaName + " [" + errorLv + "] " + errorMsg)
    return

def make_prompt(input):
    prompt_h = "{Input}を用いて、{Goal}を達成するための料理のレシピを{提案フォーマット}で回答する。\n"\
                "{Input}が食材名の場合は、その内容に応じてランダムに選んだ料理名を{Goal}とする。\n"\
                "{Input}が料理名の場合は、料理名を{Goal}とする。\n"\
                "{Input}に食材名や料理名以外が含まれる場合、料理のレシピ提案をせず、{代替案フォーマット}で回答する。\n"\
                "{提案フォーマット}と{代替案フォーマット}のどちらか一方のみで回答することを厳格に守る。\n"
    prompt_i = "{Input} = "
    prompt_c =  "/n{提案フォーマット}\n"\
                "【提案する料理】\n"\
                "{料理名}\n"\
                "【材料】\n"\
                "{食材1}:{分量1}\n"\
                "【手順】\n"\
                "{手順1}\n"\
                "{代替案フォーマット}\n"\
                "{Input}には対応するレシピを提案できません。別の食材や料理名を入力してください。"
    prompt_all = prompt_h+prompt_i+input+prompt_c
    
    return prompt_all

def use_chatgpt(input):
    prompt_ = make_prompt(input)
    
    #モデルを指定
    model_engine = "gpt-3.5-turbo"
    
    # 推論
    response = openai.ChatCompletion.create(
      model="gpt-3.5-turbo",
      messages=[
        {"role": "user", "content": prompt_}
      ])
    
    # 回答
    print(response.__dict__)
    #出力結果は、class 'openai.openai_object.OpenAIObject'であり、これを辞書形式で見込む
    answer = response.__dict__["_previous"]["choices"][0]["message"]["content"]
    logging("info", answer, "回答出力")

    return answer
    
def lambda_handler(event, context):
    logging("info", context.function_name, "実行開始")
    if json.loads(event["body"])["events"][0]["type"] == "message":
        if json.loads(event["body"])["events"][0]["message"]["type"] == "text":
            reply_token = json.loads(event["body"])["events"][0]['replyToken']# リプライ用トークン
            message_text = json.loads(event["body"])["events"][0]["message"]["text"]# 受信メッセージ
            
            #chatgptにpromptを送る
            logging("info", context.function_name, "use_api")
            response_text = use_chatgpt(message_text)
            
            print("response",response_text)
            
            # メッセージを返信
            LINE_BOT_API.reply_message(reply_token, TextSendMessage(text=response_text))

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}

コード(関数)解説
  • def logging(errorLv, lambdaName, errorMsg):

AWS CloudWatch Logsにログを残すための関数。特になくても処理には問題ない

  • def make_prompt(input):

ChatGPTに渡すプロンプトを作成する関数。引数inputにはLINEで受け付けた文字が入り、その文字を加えてプロンプトが完成し、戻り値returnとして返す

  • def use_chatgpt(input):

ChatGPT APIに値を渡し、結果をもらうためのメソッド

  • def lambda_handler(event, context):

イベントを処理するメソッドで、関数が呼び出される(LINEでメッセージを受け付けたタイミングでAWS API Gatewayに呼び出される)と、このLambda_habdlerメソッドが実行される。LINEからのメッセージは、引数eventに含まれる。

実行結果

LINEでメッセージを送った際の実行結果は以下の通り、意図通り、レシピを返してくれます。

実行結果

エラーが出た時の対処法

Amazon CloudWatchの、ログ->ロググループを選び、対象となるLambda関数を選択すると、実行毎のログを確認できます。
これで、LINEにメッセージを送った後、の結果を追うことができ、エラー処理も対応できるので、便利です。

CloudWatch

まとめ

今回は、AWSサービスとLINEを連携させ、ChatGPTを組み合わせた自動レシピジェネレーター サービスの作成方法を紹介しました。
このサービスは、食材を入力するだけで簡単に美味しい料理のアイデアを提供してくれます。ChatGPTは、豊富なレシピデータと自然言語処理アルゴリズムを組み合わせ、リアルな文章でレシピを生成することができます。実際に試してみたところ、多くの料理のアイデアを得ることができたため、日々の料理のバリエーションを増やすことができました。自動レシピジェネレーターを使うことで、手軽に料理のアイデアを得られるため、忙しい人や初心者の方にもおすすめです。
こういったサービスがどんどん世に出ていくのだなぁと思い、ChatGPTなどのAI技術の発展に今後も期待です!

AWSを活用してLINEのチャットボットを作る

AWSサービス(Lambda + API Gateway)とLINEを連携させて、チャットボットを作成した時のメモを残します。
最終目標として、LINEで尋ねたワードをChatGPTに投げて、返答結果をLINEに返すといったシステムを作ることを想定しており、
その前段階として、まずはAWSサービスとLINEを連携させるところまでを作り、LINEメッセージをオウム返しするシステムを作ります。

ほぼ同一のシステムを作られた、ありがたい先人方(以下)の知恵を借りて作っていったのですが、クラウドを用いたシステムに慣れていないところがあり、ハマった部分が各所にあるので、そこを重点的に記載して、同じミスをしないようにする。
AWS Lambda + Python + LINE Botで傘が必要か教えてもらう - Qiita
AWSを活用してLINEチャットボットを作ってみた!① | リウコムTechブログ
Lambdaを使用してLINEとChatGPTを連携してみた。 - Qiita


目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
・LINE (developers)

開発手順

  1. LINE Message APIを使うための環境を整える
  2. AWS の環境を整える+LINEと連携する
  3. AWS Lambdaでコード実行(動作確認)

実装

LINE Message APIを使うための環境を整える:様々な方がやり方を説明している為、簡潔に説明

  • LINE Developersに登録する
  • チャネルを作成する

まずは、LINE Developersに登録します。(URL)
その後、公式ドキュメントの記載に従って、チャネルを作成します。今回は、メッセージのやり取りをしたいので、Messaging APIチャネルを選択して、作成する。
作成が完了したらこのステップは終了!ただ、後のことを考えて、"Messaging API設定"の一番下、"チャンネルアクセストークン"を押下し、その値は控えておく。
Message APIを使用するときに、認証が必要であり、チャンネルアクセストークンはその認証手段として利用する。

LINE Developer


AWS の環境を整える+LINEと連携する:結構苦戦したので、詳しく説明

AWS Lambda関数の作成

AWSコンソールにログインして、Lambdaの画面を開き、関数->関数作成を押下する。
以下のような画面が出るので、関数名は適当に記入、ランタイムは"Python3.9"を設定する。

関数

タイムアウト設定

デフォルトは5秒に設定されていますが、API等 別のシステムとやり取りをする場合は、タイムアウトが起こり得るため、30秒程度まで伸ばした設定に変更する。

AWS Lambdaレイヤー作成

AWS Lambda上で使えないモジュールがある場合は、レイヤーを作成して対応する。
今回は、LINE BOT開発用のline-bot-sdkをインストールして、レイヤーとして設定する必要があります。
また、ChatGPTの使用を想定すると、openaiもインストールして、同様にレイヤーとして設定する必要があります。

  • ローカルでpipコマンドを使って必要モジュールファイルをダウンロード


以下コードでは、"python”フォルダを作成して、そのフォルダに必要モジュールファイルを格納、zip化する

$ mkdir python
$ cd C:\Users\user\python
$ pip install line-bot-sdk -t ./
$ pip install openai -t ./ #openaiモジュールもレイヤーに追加する場合
$ cd ../
$ zip python.zip python/* #zip化する

  • zipファイルをアップロードする

初歩的なのですが、フォルダ構成を間違えて、lambdaで必要なモジュールのimportでエラーが出来ないことで詰まりました。
フォルダ構成は以下(URL)の通りに設定して、アップロードする必要があります。
zipファイルの構成は以下の通り

@@.zip
  -python
    |-@@@
    |-@@@

  • 作成したzipファイルをLambdaレイヤーに設定する。
レイヤー作成

環境変数の登録

環境変数の役割: 環境毎に値が変わるものの管理に使用し、ソースの修正の手間を省かせる。
特定パラメータ(API Key等)を環境変数に設定して、呼び出して使う。

環境変数の設定方法は、URLを参考にする。
今回のシステムでは、以下2点(Openai_ACCESS_TOKENはChatGPT APIを使用する時だけ使用)を環境変数として設定した。
キーは任意に入力し、値にAPIキーを入力して、コード内で呼び出して(LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']という感じ)使う。
LINE Message APIのチャンネルアクセストークンはキー: LINE_CHANNEL_ACCESS_TOKENの値に入力して、今回は設定した。

環境変数

API Gatewayの作成/設定

ここがかなり苦戦したので、詳しく説明していくが、まだ完全に理解できておらず随時付け足す。

Lambdaの関数画面で、以下画像では左下に"API Gateway"という文字とアイコンが存在するが、初期は存在しない。
"+トリガーを追加"個所をクリックして、ウィンドウを開く。

トリガー追加


ウィンドウが開いたのち、"API Gateway"を選択すると以下画像のような設定画面が現れる。

詳細設定

今回はAPIタイプ REST API/セキュリティは"開く"を選択して使用する。
正直APIの種類については分かっていない部分が多く(以下のようなwebページで勉強したのですが・・・)、後々勉強して記載していく。
qiita.com
また、セキュリティに関しても本来は、IAMポリシーを使って使用ユーザーを制限することがベストプラクティスみたいですが・・・一旦は全てに開放する設定にしています。
Amazon API Gateway のセキュリティのベストプラクティス - Amazon API Gateway

上記の通り作成すると、先程の関数ウィンドウのトリガー欄に、API Gatewayが作成される。

API Gateway


API Gatewayの名前をクリックして、詳細を設定する。
以下画像では、"Resources"欄に"/POST"という1つしか存在しないが、当初は"ANY"が存在している。
"ANY"はすべてのHTTPメソッドをサポートしており、そのままでもこのような目的のために使えるのでは?と思っているが、具体的な設定方法が分からなかった為、先人たちの方法にしたがって対応する。
"ANY"メソッドは削除して"POST"メソッドを作成する。POSTメソッドは、HTTP POSTリクエストをサポートしており、HTTP POSTとは、クライアントからの入力内容をWebサーバに送信するために使用するメソッドのこと。

詳細設定


POSTメソッド作成時の設定は以下の通り、"Lambda Function"の入力を求められるが、そこにはLambda 関数名を入力する。

POST

作ろうとしているシステムでは、AWS API GatewayへのHTTP POSTリクエストはLINEから来ることを想定しているが、LINEから来たかどうかの検証をする方法として、署名の検証方法が推奨されているようなので試しに入れてみる。恐らく、この設定がなくても問題なく動作すると思っているが、検証はしていない。もし間違っていたらコメントで教えていただけますと嬉しいです。
メッセージ(Webhook)を受信する | LINE Developers

設定方法は、"Method Request"を選択

メソッドリクエス


HTTP Request Headers欄に"X-Line-Signature"を入力して設定する。

メソッドリクエスト詳細

  • API のデプロイ

"Resources"の横の"Actions"欄から、Deploy APIを選択することで、APIをデプロイ(設置)することができる。
これを忘れて、次工程がうまくいかず、、、参考までにデプロイ方法はこちらのURLに載っています。
デプロイ時のStage名などは、任意に設定する。

  • Webhookの設定

"Stages"選択し、デプロイ時のStage名を選択すると、以下のような画面が現れる。
Invoke URLがリクエストするURLになるので、ここにLINEの送信先が来るように設定する。

リクエスト確認


Webhookとは、ウェブアプリケーションの機能の1つで、外部のサービスやアプリケーションに対して、特定のイベントが発生した際に自動的に通知を送信することができる仕組み。
今回では、LINE上でメッセージを受けた際にそのメッセージと通知の連絡をAWS API Gatewayにリクエストする際に使う。以下の通り、LINE DevelopersのページでWebhook設定個所があるので、上記API GatewayInvoke URLを入力し、"検証"ボタンを押下する。その結果、"成功"と記載されたウィンドウが出れば、問題なく設定できている。

Webhook設定

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
LINEからはjson形式のリクエストが送信される。json形式というのをすっかり忘れており、json形式の読み込みでも少しはまってしまった・・
jsonデータは"json.loads"を使うことで、辞書dictなどのオブジェクトとして読み込むことができ、処理ができる。

import json
import urllib.request
import os
from linebot import LineBotApi
from linebot.models import TextSendMessage
from datetime import datetime 

#######APIキーを取得
# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

########説明
#LINEで入力されたテキストは ['events’][0]['message’]['text’] に入っている
#受信したメッセージに対してリプライを行うには、replyTokenの値を使用する

# ログ出力関数
def logging(errorLv, lambdaName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
    print(loggingDateStr + " " + lambdaName + " [" + errorLv + "] " + errorMsg)
    return

def lambda_handler(event, context):
    logging("info", context.function_name, "実行開始")
    logging("error", context.function_name, "エラーログテスト")
    if json.loads(event["body"])["events"][0]["type"] == "message":
        if json.loads(event["body"])["events"][0]["message"]["type"] == "text":
            replyToken = json.loads(event["body"])["events"][0]['replyToken']# リプライ用トークン
            messageText = json.loads(event["body"])["events"][0]["message"]["text"]# 受信メッセージ
            LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=messageText))# メッセージを返信(受信メッセージをそのまま返す)

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}


Lambda関数を実行する際、手動で実行(Test)する際は、"Execution results"欄(以下画像)に実行結果が吐き出されるが、今回ケースの場合のエラー確認方法に困った。

Test


そう、今回の系は、LINEにメッセージを送り、そのリクエストがAWS API Gatewayに送られ、Lambda関数が実行される。こういうケースの場合、どこにエラー結果が表示されるのだろう・・・
そうですAmazon CloudWatch この存在を知って助かりました。

Amazon CloudWatchの、ログ->ロググループを選び、対象となるLambda関数を選択すると、実行毎のログを確認できる。
これで、LINEにメッセージを送った後、の結果を追うことができ、エラー処理も対応できる。

CloudWatch

実行結果

LINEでメッセージを送った際の実行結果は以下の通り、意図通り、オウム返しをすることができました。
>

実行結果

まとめ

今回は、AWSサービスとLINEを連携させ、LINEメッセージをオウム返しするシステムを作りました。
次は、ChatGPTと連携させて、ChatGPTへの質問結果をLINEで返すところまでをまとめようと思います。

フィルタの設計_メモ

フィルタ設計について、関連用語等を簡単にまとめます。

目次

フィルタ設計について

フィルタ設計は、以下項目を検討します。

  1. 周波数特性(フィルタ種類)
  2. 設計仕様(フィルタ仕様)
  3. (アナログ回路の場合)フィルタ回路を構成する素子や接続方法
  4. ノイズ除去等、入力データの処理

周波数特性(フィルタ種類)

振幅特性に対する設計要求が与えられることが多く、通過域や阻止域の周波数によって、以下のように呼ばれ方が変わる。

  • 低域通過(ローパス)フィルタ
  • 高域通過(ハイパス)フィルタ
  • 帯域通過(バンドパス)フィルタ
  • 帯域阻止(バンドエリミネーション)フィルタ

 阻止域の幅が狭いものはノッチ・フィルタと呼ばれる

  • 全域通過(オールパス)フィルタ

 移送特性を変化させる目的で使われる

フィルタ種類

設計仕様(フィルタ仕様)

その他設計項目として、以下が存在

  • パスバンド(角)周波数
  • ストップバンド(角)周波数
  • パスバンド リップル
  • ストップバンド減衰量

フィルタ仕様


上記任意の値の仕様を満たす伝達関数を求め、フィルタとして使う。
伝達関数の求める方法として、多くの近似手法があり、代表的な近似結果が以下の通り。


それぞれのフィルタの特徴は、以下記事に示す。
dango-study.hatenablog.jp

ノイズ除去等、入力データの処理

観測したい信号にノイズが含まれる場合は、ノイズを除去することが求められる場合がある。

ノイズ除去方法としては、以下の通り。

  • 特定の周波数成分を抽出

ローパスやハイパス等のフィルタを組み合わせて、通過周波数帯を選択する。この手法は、信号とノイズとがそれぞれ異なる周波数である場合に有効。

  • 特定の周波数成分を除去

特定の決まった周波数を持つノイズが信号に入る場合は、ノッチフィルタが有効なケースがある。このフィルタは、特定の周波数成分だけを除去し、他の周波数帯には大きな影響を与えない。ただし、上記同様に信号とノイズとがそれぞれ異なる周波数である場合に有効。

畳み込み型 ニューラルネットワーク


続いて、畳み込み型のディープラーニングアルゴリズムをまとめていきます。
このアルゴリズムは画像処理において広く使われており、計算負荷を軽減する仕組みが多く導入されています。

目次


Convolutional Neural Network(CNN)の概要

このアルゴリズムでは、畳み込みというフィルタ処理を含んでおり、畳み込みによる出力が次の層の入力として使用されます。このフィルタによって、明るさやエッジなどの特徴、物体を定義する特徴の抽出が可能となります。

畳み込み処理

入力に対して、フィルタを用いて内積計算を行うことに相当し、入力に対してより小さいサイズに畳み込んで出力されます。

図:畳み込みの計算処理
f:id:toku_dango:20220101140754p:plain

通常のニューラルネットワークでは、入力ごとに重みが異なりますが、畳み込みではフィルタごとに重みが変わるため、重みを使いまわして計算することになります。
特徴抽出・計算を軽くするための処理として利用されるため、出力サイズは入力サイズと比較して小さくなることがポイントです。ただ、小さくなりすぎると計算ができなくなる/層を深くすることができない(その分小さくなるため)などの問題があり、それに対応して、paddingという方法がアルゴリズムに含まれるケースもあります。

padding(zero-padding)

畳み込むときに、入力画像の周りに0埋めの余白をつけるて計算する処理

pooling

入力情報を圧縮処理であり、最大1/2の大きさに入力サイズを圧縮することが可能です。
畳み込みでは、3×3の小さいフィルタを使うケースが多いため局所的な処理しかできないが、Poolingでは広い範囲の情報を含んだまま大きく圧縮することができます。デメリットとしては、入力が小さく圧縮されてしまうため、実施回数に限界があるということです。

Dilated Convolution

特殊な畳み込み処理であり、通常の畳み込みと比較して広い範囲の情報を含んだまま圧縮することができ、Poolingと比較して出力が小さくなりすぎないという利点があります。

図:Dilated Convolutionの計算処理
f:id:toku_dango:20220101150721p:plain

UpConvolutin(Deconvolution)

通常の畳み込み処理では、出力の方が入力よりも小さくなるが、この方法は入力よりも出力を大きくする処理になります。

多チャンネル時の処理

多チャンネルとは、フィルタ数が複数になったケースを想定しており、上記の通りの計算方法では、チャンネル数分計算量が倍になり、負荷が非常に重たいです。この計算を効率的に実施する方法として、im2colというテクニックがあります。
※im2colとは:入力やフィルタの2次元領域を、行列の列/行に変換して計算をする手法。GPUは行列計算に長けており、計算方法を変えることで負荷を軽減する。

学習手法

CNNでは、計算量が膨大になるケースが多いため、効率よく学習を実施する必要があり、その手法として以下方法が提案されています。

  • Batch Normalization: チャンネル毎にバッチ内の全データを使って平均/分散を求めて正規化する手法
  • Instance Normalization: 入力毎に平均/分散を求めて正規化する手法(バッチサイズが1のBatch Normalization)
  • Layer Normalization: 全チャンネルにまたがり、平均/分散を求めて正規化する手法で、画像毎に平均/分散を求めることと同じ
  • Group Normalization: Layer NormとInstance Normを組み合わせたもので、チャンネルを任意数に分割して、それぞれ平均/分散を求めて正規化する手法


転移学習(Transfer Learning)

別タスクで学習したネットワークを使用して、新しくタスクを学習する手法で、似たものの分類や認識に使ったネットワークを転移学習すると、その学習回数を減らすことができる利点があります。

CNNの代表的なモデル

画像認識で使われるCNNの代表的なモデルとして、以下が提案されています。

  • AlexNet: 物体認識に初めて深層学習の概念/畳み込みニューラルネットワークの概念を取り入れたモデル [2012年のILSVRCで優勝したモデル] ※ILSVRCは大規模画像認識の競技会のこと
  • VGG16: 畳み込み13層, 全結合3層からなるシンプルなモデル [2014年のILSVRCで提案されたモデル]
  • GoogLeNet: モジュールという特定の組み合わせの層を積層させたモデル[2014年のILSVRCで優勝したモデル]
  • Residual Networl(ResNet): 現在のCNNのベースとなるモデル。超多層のネットワークであり、超多層での計算を可能にした手法
  • DenseNet: ResNetを踏まえて開発されたモデルで、前方の各層からの出力全てが後方の層の入力として使用される手法
  • ResNeXt: ResNetのブロック内で入力を分岐させて並列に処理する手法
  • MobileNet: スマートフォンにのせれる程度の小さなモデル
  • UNet: 画像の出力で使用されるモデル


Object Detection(画像のどこに何があるか)を推定する目的で使用されるモデルとして以下が提案されています。
色々なウィンドウサイズで画面を走査(スライディングウィンドウ)が主流の方法であるが、非常に時間がかかる。

  • Regions with CNN features(R-CNN): Selective Searchと呼ばれる領域を絞り込むという機能を含めたモデル
  • Fast R-CNN: ROI Poolingと呼ばれる入力を固定サイズに切り分け/プーリングを行うことによって計算量を大幅削減したモデル
  • Faster R-CNN: Fast R-CNNを改善したモデルで、途中の演算を共有することによって演算コストを最小化するモデル
  • You Only Look Once(YOLO): Faster R-CNNよりも制度は落ちるがリアルタイム計算を可能にしたモデル
  • Single Shot MultiBox Detector(SSD): YOLOと似た精度/速度を誇るが、YOLOの弱点であった小さい物体の検出にも強いモデル

まとめ

今回は畳み込み型ニューラルネットワーク CNNに関する概要をまとめました。
計算には大きな負荷がかかり、円滑に計算する手法が多く編み出されているのですね。

回帰結合型 ニューラルネットワーク


続いて、回帰結合型のディープラーニングアルゴリズムをまとめていきます。


目次


Recurrent, Recursive Neural Network(RNN)の概要

通常のニューラルネットワークでは、ある層の出力は、次の層の入力にのみ利用されます。
RNNでは、ある層の出力が、次の層の入力として利用されるだけでなく、別のニューラルネットワークの入力としても利用されます。上記説明の構造を持つニューラルネットワークをRecurrent Neural Networkと呼び、RNNと言えばこちらの意味を指すことが多いようです。

図:Recurrent Neural Networkの構造(1層の構造)
f:id:toku_dango:20211226162313p:plain

もう一つのRNNであるRecursive Neural Networkは、Recurrentのものとは異なり、木構造のネットワークを持っており、自然言語処理などで使用されます。本記事では、Recurrent Neural Networkについて以下の通りまとめていきます。

RNNのアルゴリズム

上記の通り、出力が入力で使用される構造を持つニューラルネットワークで、主に時系列処理で用いられます。
対象となる時系列データには色々と種類がありますが、数値データだけでなく音声やテキストなども含まれ、数値解析や翻訳などでも利用されるようです。

RNNの学習について

出力を再度入力として使用する構造のため、どのように重みを計算するのか、RNNではBack Propagation Through Time(BPTT)という方法で解決しています。BPTTは、出力を再度入力として使用するループの数だけ次元を拡張して、入力/出力を多次元の形式に構造変換して計算可能な状態にする方法です。

図:BPTTのための考え方
f:id:toku_dango:20211226162525p:plain

RNNの課題

出力を再度入力として使用するが故に、構造が大きくなりすぎてしまうという問題があります。
上記より、古い情報を上書きするような構造に調整することを前提としており、それによって今度は勾配が消える、発散するという問題が生じます。
勾配の発散については、勾配クリッピングで抑制できるが、消失に対しては対策が取りにくく、本アルゴリズムの課題となります。重みの大きさは、勾配の発散に対応するために1以下に設定するが、古いデータはその重みを何回も掛けることとなり、アルゴリズム複雑になってしまうと、このような重ね合わせによって勾配が消失してしまいます。
上記を踏まえ、重みを1付近に設定すると、今度はネットワークの表現能力が制限され、本来の表現能力が発揮できなくなるため、課題解決用のアルゴリズムが追加で必要となります。

スキップ接続

一つ前の出力が現在の入力として使われる構造、これを一つ前の出力ではなく、二つ前、それ以前の出力を入力として使うように変更する方法になります。これにより、より以前の情報が現在の入力として入るまでに計算される回数が減るため、データの消失による問題を軽減されます。

また、これに付随した方法として、接続を削除するという方法があり、短い時間間隔でのループ処理だけを削除して、より時間が離れたリンクのみを残すことで、この問題に対応できます。

RNNの応用

RNNをベースとしたアルゴリズムを紹介します。

  • Bi-directional RNN: 過去⇔未来の各方向のRNNモデルでアンサンブルする方法
  • Long-Short Term Memory(LSTM): 勾配消失への対応として情報を記憶しておくCEC(メモリセル)を有したRNN
  • Gated Recurrent Unit(GRU): LSTMをシンプルな形に変えたモデル
  • Sequence-to-Sequence(seq2seq): Encoder部分とDecoder部分に分かれているNNを系列データに利用したモデル



アルゴリズムの構造は以下にまとめられてるので、詳細は以下を確認ください。
deepage.net

Bi-directional RNN

過去の出力を未来の入力として使うという、過去から未来という方向がある程度RNNにはありましたが、これを双方向につないだ構造を持つRNNとなります。双方向だからといって、モデルが双方向に伝達するような作りになっているわけではなく、順方向/逆方向のモデルをそれぞれ作り、結果をアンサンブルする方法をとります。
例えば、自然言語処理では、言葉の最後に言葉の最初が従属するようなケースがあり、そういったケースでは双方向につないだネットワークの方が問題を解くことに適しているケースがあるようです。

Long-Short Term Memory(LSTM)

基本的な構造はRNNと同じですが、情報を記憶するための機能(CECやメモリセルと呼ばれる)を持っていることが特徴となります。重みを大きく調整するような必要な情報は保持して、無関係な情報は記憶しないという"入力重み衝突(input weight conflict)"の機能を持っており、先程紹介した勾配消失の課題に対して対応可能となります。

Gated Recurrent Unit(GRU)

LSTMをシンプルな形に変えたモデルで、メモリセルを無くし、メモリ機能と入力部を合体した構造を持っています。

Sequence-to-Sequence(seq2seq)

翻訳用のモデルとして使われており、Encoder部分とDecoder部分とで構成されます。
このような構造のNNモデルがEncoder-Decoderと呼ばれており、Encoder部分は入力を中間表現(ベクトル)に変換する役割、Decoder部分はそれを出力に変換する役割を示します。

まとめ

今回は回帰結合型ニューラルネットワーク RNNに関する概要をまとめました。
目的に応じてどういったモデル構成が必要か、把握して活かしていきたいですね。

ディープラーニング設計について


前回の記事までで、ディープラーニングアルゴリズムや構造について記載しました。
続いては、設計時に必要な考え方についてまとめます。

目次


学習の円滑化1(パラメータ(重み)の初期値について)

ディープラーニングでは、SGDなどのパラメータ更新アルゴリズムを駆使して最適解を目指します。
よって、重み更新の初期値はかなり重要です。もし最適解に近い重みからスタートできれば、更新回数は少なく・最適解へたどり着ける可能性もずっと高くなります。

図:ディープラーニングの重みと損失関数値の関係
f:id:toku_dango:20211215204904p:plain

初期値設計の考え方にHeの初期化、Xavierの初期化(Glorotの初期化)と呼ばれる方法があり、
ここで紹介をしていきますが、まずはやってはいけない初期値設計について簡単に説明します。

  • すべての重みを均一にする

もしこのように初期値を設定してしまうと、すべての重みが連動して同じ値に動く可能性が生じます。
これの何がいけないかというと、すべてが連動して動いてしまうとそれがあたかも一つの変数のように動いてしまうという点です。
ディープラーニングの特徴である柔軟性を失いかけるので、重み初期値をランダムに設定するということは大変重要です。

  • 勾配消失問題

活性化関数としてシグモイド関数、ハイパボリックタンジェントを使用する場合、勾配>0となる場所は限られてきます。
初期値に勾配=0となる個所を選択してしまうと、勾配消失問題を引き起こし、パラメータ更新が行われないという問題が生じます。
上記より、勾配が>0となる個所を選択すれば良いということになりますが、紹介した活性化関数では、勾配>0となる個所は限られており、先程説明したような重みが一体化して動いてしまうという現象を引き起こしかねないので、注意が必要です。

図:代表的な活性化関数のグラフ(左から順に線形関数、ReLu関数、シグモイド関数、ハイパボリックタンジェント)
オレンジ:活性化関数を微分したもの(導関数)
f:id:toku_dango:20211204230142p:plain

Xavierの初期値

一般的なディープラーニングフレームワークで標準的に使われている手法です。
この初期化の考え方は、各ノードで計算された出力値(アクティベーション)の偏りをなくすという考え方であり、「前層のノードの個数をnとした時に\frac{1}{\sqrt{n}}標準偏差を持つ正規分布を使う」というものです。
各層のアクティベーションの分布をある程度広がるように調整することで、学習がスムーズに行えるように調整します。

この手法は、シグモイド関数やハイパボリックタンジェントのような、左右対称で中央付近が線形関数とみなせるものに適しています。

Heの初期化

ReLu関数を活性化関数として使用する場合に有効な方法です。
この手法も基本的な考え方はXavierの初期化と同じであり、「前層のノード個数をnとした時、\sqrt{\frac{2}{n}}標準偏差とする正規分布を使う」というものです。
ReLu関数の場合は、負の領域の勾配が0となるため、より広く値を分散させる必要があります。

学習の円滑化2(BatchNormalization)

各層のアクティベーション分布がある程度の広がりを持つように重みの初期値を設定しました。
次に、各層で適度の広がりを持つように、分布の調整を行う手法(Batch Nrmalization)を紹介します。
というのも、ミニバッチ学習などを行った場合、毎回パラメータ更新に使用するデータが異なります。
このデータの分布が異なる場合、損失関数の低下に時間がかかり、学習がうまくいきません。
このように分布が変化することを内部共変量シフト(共変量シフト, Internal Covariance Shift)と呼びます。

Batch Normalizationの仕組み

「各バッチ、各層で平均と分散の正規化実施」これがBatch Normalizationの仕組みとなります。
主な利点は、内部共変量シフトが解消され、学習率を上げても学習できること、過学習を抑制(正則化の必要性が下がる)することです。



\mu_{B}=\frac{1}{m}\sum_{i=1}^{m}{x_{i}} 平均値計算\\
\sigma^{2}_{B}=\frac{1}{m}\sum_{i=1}^{m}{(x_{i}-\mu_{B})}^{2} 平均値計算\\
\hat{x_{i}}=\frac{x_{i}-\mu_{B}}{\sqrt{\sigma^{2}_{B}+e}} :パラメータ更新
eはゼロ除算を避けるための小さい値

入力データは平均0の分散1の分布に収まるように標準化されます。

内部共変量シフト

各層のアクティベーション及び入力分布が変わってしまうことが問題であり、このような状態を指す。
内部共変量シフトが起きている場合、学習を阻害する場合があり、従来はこういったケースに学習率を小さくする/重みの初期値設定を調整する等の対応を実施していた。

過学習への対応

機械学習等の柔軟なモデルを使用する場合は、過学習について気を付ける必要がある。
過学習とは、学習データに合わせこみすぎてしまい、汎化性能を失ってしまうことであり、主な原因として以下があげられる。

  • パラメータが多量で柔軟な表現ができる
  • 訓練データが少ない


これに対応するやり方として、正則化やDropOut、画像データであればデータの拡張などがあり、その方法を紹介します。

正則化(Weight decay)

モデルの学習は、損失関数の値を小さくすることを目的に実施します。
この手法は、その損失関数の値に「重みを追加する」という手法となります。
このように、重み自体の値が大きくならないように、損失関数にペナルティーを科すという考え方です。

L1正則化 Lasso

この手法は、損失関数の値にL1ノルムを加える手法です。
L1ノルムは、重み絶対値をすべて足し合わせたものになります。



E^{'}(W)=E(W) + \lambda×|w| :L1正則化を加えた損失\\
E(W) :損失関数\\
\lambda×|w| :L1ノルム
この手法は、パラメータをスパースにする効果があり、過学習の抑制以外にも効果があります。
スパースとは、重み=0になることを示し、メモリ使用量の削減/計算量の削減など効果があります。

L2正則化 Ridge

この手法は、損失関数の値にL2ノルムを加える手法です。
L2ノルムは、重み絶対値の二乗をすべて足し合わせたもの(ユークリッド距離に該当するもの)になります。



E^{'}(W)=E(W) + \lambda×|w|^{2} :L2正則化を加えた損失\\
E(W) :損失関数\\
\lambda×|w|^{2} :L2ノルム

DropOut

過学習を抑制する方法として、Weight decayという方法を説明しました。
Weight decayでもある程度対応は可能ですが、ニューラルネットワークが複雑になると、正則化項を加えたとしても過学習が起きてしまいます。
このようなことに対応する方法として、次にDropOutという手法について説明します。

この方法は、ニューロンをランダムに削減しながら学習する手法です。
データで学習をするたびに、中間層のニューロンをランダムに選んで削除して、実際に学習後にテストを実施する際は削除せずに全てのノードを使用します。
このような手法を使うことで、Baggingのような効果が得られ、過学習が抑制できます。
Baggingを用いたモデルの学習方法をアンサンブル学習と呼び、機械学習ではよく用いられます。

データの拡張

データ数が限られると過学習してしまう危険があるので、データ数を増やすことで過学習を防ぐ手法です。
主に画像データで用いられ、画像を回転/反転/拡大縮小/色の変換などをすることで一つの画像を増やすことが可能になります。

また、似た考え方の別の手法として、半教師あり学習の手法についても説明します。

教師あり学習

目的次第ですが、モデルの学習には、ラベルとセットであるデータが必要です。
ただ、ラベルがあるデータの入手には限界があり、ラベルがないデータならば多く取得できるケースがあります。
教師あり学習は、ラベルがないデータを生かした学習手法であり、分類やクラスタリングで使われています。
基本的な考え方は、ラベル毎にデータが分布するという前提から、データが疎になっている所を分類界面として捉える手法です。

Early Stopping

その名前の通り、学習を途中で止めるという手法。
正則化が不十分な場合、ミニバッチ学習で学習を数回行っていくうちに、損失関数の値が大きくなるケースがある。
これは、その数値が現れた該当データの前で過学習が起きていることを示しており、このような現象が見られた時に学習を止める手法をEarly Stoppingと呼んでいる。

** パラメータの共有
畳み込みニューラルネットワークで使われる手法であり、フィルタをすることでパラメータ数を削減する手法。
これにより、モデルの表現を制限することで過学習を減らし、副産物として処理負荷も下げることができる。

まとめ

今回はディープラーニングの設計で必要な考え方についてまとめました。
学習を円滑に行う、過学習を防ぐといったことが大きな課題であり、それに対応する手法が非常に多く研究されていたことが分かりました。
次も関連知識についてまとめていきたいと思います。

誤差逆伝播法


前回はニューラルネットワークの構造について説明をしました。
続いては、モデル構築に必要な学習に関する技術を説明します。

目次


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

以下画像のようなニューラルネットワークがあったとして、入力x及び出力yの関係は以下の通りです。
この入出力の関係の組み合わせで複雑なモデルが構築できるということでしたね。このモデルをデータにフィットさせる(学習させる)ためには重みwをデータに合わせて変える必要があります。

図:ニューラルネットワーク
f:id:toku_dango:20211212100709p:plain

勾配降下法

このような関数のパラメータ(係数や重み)を決めるアルゴリズムとして、勾配降下法が使われるので、もう一度紹介します。
勾配降下法は、「モデル関数に対してパラメータ(係数)を更新していって、最適解を目指すこと」が勾配降下法のアルゴリズムです。

ただ、このアルゴリズムをそのままディープラーニングで使用するのは、以下理由からかなり難しい。

  1. 重みw計算する場合、重みw一つ一つを個別で最適化する必要があり、計算効率が悪い
  2. 凸が複数存在する場合があり、最終的な凸が最適解であるかは分からない(以下図の通り)


図:ディープラーニングの重みと損失関数値の関係
f:id:toku_dango:20211215204904p:plain

これらの問題をそれぞれ解決するための方法として以下方法があげられる。
1. に対する対策:誤差逆伝播法(Back propagation)
2. に対する対策:確率的勾配降下法(SGD)等

誤差逆伝播

この方法を使用する前は、「重みwを一つ一つを少し更新して、損失関数の変わり方をチェック(勾配計算)、重みの更新を繰り返す」という方法が用いられていた。つまり偏微分をひたすら計算することになるということであるが、本方法はこの計算を効率的にできるようにした方法だ。

以下図のようなニューロンの計算を考えてみる。
計算したいのは、出力層の損失関数の重み変化に対する勾配。
基本的に足し合わせや掛け算の計算が左から右に流れる構造であり、左の結果が右の結果に影響する。
誤差逆伝播法は、この流れを逆に使用した方法だ。
詳細は以下URLを参照して簡単にまとめると、出力層に入力される値の重みは、以下図のような計算だけしか間に挟まないため、簡単に計算にて求めることができる。
より入力層に近い所の重みについては、上記計算過程で求めた誤差値を一つ前の層の出力値の誤差として扱って計算し、これを連鎖的に入力層側に向かって全ての重みを求める。このような重みを連鎖的に求めることができ、計算負荷を軽減できる方法が誤差逆伝播法だ。

図:ニューロンの計算
f:id:toku_dango:20211212100859p:plain

qiita.com

勾配消失

前回の記事でも紹介したが、誤差逆伝播法は出力層から入力層に向けて誤差値を伝播させて計算させる方法であるが故、誤差=0となってしまった時点で勾配=0となってしまい、該当層からさらに入力層に近い層の誤差も0となってしまう。
勾配=0となると、重みwの更新が不可能となるため、勾配消失に対する対策が不可欠となる。

勾配消失への対策方法としては、以下方法があげられる。

  • ReLu関数等の勾配が消えない活性化関数を使う
  • 重みの初期値を設計する
  • 入力値の正規化を行う


確率的勾配降下法(Stochastic Gradient Descent :SGD)

SGD の基本的な仕組みは勾配法と同じであるが、パラメータ更新毎にデータをランダムサンプリングするという所が改良ポイント。
ランダムサンプリングするということで、全データを使用しないため、再学習の計算量が低くなるという利点もある。
具体的には以下方法をで計算を行う。

  1. 損失関数f(x)に対して、学習率ηとxの初期値を定義する。
  2. データをランダムサンプリングする(ミニバッチ)
  3. データをもとに勾配(s)計算
  4. 計算した勾配(s)×η分だけxを変化させる
  5. 2~4を繰り返す



g_{t}=\frac{dL(w)}{dw} :勾配計算\\
w_{t+1}=w-ηg_{t} :パラメータ更新
SGDはデータをランダムにピックアップして重みを更新するアルゴリズムであり、使用データは1つづつしか使わないことと説明しているページあり。
ミニバッチで学習する方法を"ミニバッチSGD"と、SGDと区別している。

ミニバッチとは

データをすべて使って学習を行うことをバッチ学習と呼び、
データの中からランダムにn個を選び、選んだデータで学習を行うことをミニバッチ学習と呼ぶ。
特に上記にn=1でデータを選ぶ方法はオンライン学習と呼ばれる。

ミニバッチ学習の利点は、バッチ学習と比較して計算リソースが少なく、オンライン学習と比較して重みの更新が安定すること

確率的勾配降下法の改善アルゴリズム

それぞれのアルゴリズムの特徴は、簡単に書くと以下の通り。

  • Momentum: SGDのパラメータ更新量を移動平均的な動きに調整
  • Nesterovの加速勾配降下法: 確実に損失関数値が減少する方向にパラメータ更新量を調整
  • AdaGrad:パラメータ更新量に応じて学習率を調整
  • PMSProp:AdaGradアルゴリズムを修正して学習率が低くなりすぎないように調整
  • Adam: MomentumとPMSPropを組み合わせたアルゴリズム

Momentum

過去の更新方向から、更新量を調整するテクニック
Momentum(慣性)と言われるように、重みが更新されている方向に更新量が上澄みされる。
重み更新の動きが、移動平均をかけたような感じになる。

Momentumを含めたSGDアルゴリズム



g_{t}=\frac{dL(w)}{dw} :勾配計算\\
m = ηg_{t} + γm:パラメータ更新量調整\\
w_{t+1}=w-m :パラメータ更新

γは、前回の勾配値の重みであり、パラメータ更新量mは通常のSGDから移動平均をかけたような動きを示す。

Nesterovの加速勾配降下法

Nesterov Accelerated Gradient :NAGと呼ばれ、Momentumを改善したアルゴリズム
パラメータ更新量の方向が正しい方向を向いているかを確かめた上でパラメータを更新するという考え方。
Momentumだけでパラメータ更新した後の勾配を確認して、パラメータを更新する。



g_{t}=\frac{dL(w-m)}{dw} :勾配計算\\
m = ηg_{t} + γm:パラメータ更新量調整\\
w_{t+1}=w-m :パラメータ更新

先にMomentumの項で重みを変更させて勾配を計算している。

上記の通り、アルゴリズムを分けて説明したが、すべて含まれたものをSGDとして扱うことも多い。
それぞれの処理の動きをアニメーションで示したページがあったので、紹介する。
qiita.com

AdaGrad

SGDでは学習率ηは常に固定であるが、この調整を加えたアルゴリズム
更新回数に応じて学習率を変更する。基本的には、学習回数が多い場合は学習率を下げる。



g_{t}=\frac{dL(w)}{dw} :勾配計算\\
h = h + g_{t}×g_{t}:パラメータ更新量調整\\
w_{t+1}=w-η\frac{g_{t}}{\sqrt{h}+e} :パラメータ更新

パラメータeは、0除算を防ぐためのきわめて小さい値です。

RMSProp

AdaGradと似た学習率を変更するモデル。
AdaGradでは、パラメータの勾配の二乗和を使用する為に必ず正の値となるため、学習率は必ず下がる。
このアルゴリズムでは、学習率が小さくなりすぎないように対応している。



p < 1\\
g_{t}=\frac{dL(w)}{dw} :勾配計算\\
h = p×h + (1 - p)×g_{t}×g_{t}:パラメータ更新量調整\\
w_{t+1}=w-η\frac{g_{t}}{\sqrt{h}+e} :パラメータ更新

パラメータhは、学習率を調整するためのパラメータであり、計算式より移動平均をかけた値になっている。
また、計算内には勾配値の二乗が含まれており、学習値が小さくなりすぎないように調整がされている。

Adam(Adaptive Moment Estimation)

RMSPropを改良し、momentumの要素を加えたアルゴリズム



p < 1\\
g_{t}=\frac{dL(w)}{dw} :勾配計算\\
m = ηg_{t} + γm:パラメータ更新量調整 Momentum\\
h = p×h + (1 - p)×g_{t}×g_{t}:パラメータ更新量調整 PMSProp\\
w_{t+1}=w-η\frac{m}{\sqrt{h}+e} :パラメータ更新

まとめ

今回は逆誤差伝播法についてまとめました。
アルゴリズムの動きをコードで確認している最中なので、それが完成したら公開しようと思います。