世界一わかりみの深いAzure OpenAI Service

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【6/19開催】Kong Community Japan Meetup #4
本イベントでは、Kong Inc. のVP of ProductであるReza Shafii氏もプレゼンターとして参加。当社からはアーキテクト マネージャーの槌野の登壇が決定!参加無料です!!
https://column.api-ecosystem.sios.jp/connect/kong/1081/

【6/21開催】開発者目線でのSBOMとの向き合い方
SBOMの導入から開発者がSBOMの作成・管理を自動で行っていくための方法(デモ)を紹介します。SBOMを全く知らない人から、開発との統合までを紹介するので様々なレベルの方に学びがあるライブとなる予定です!
https://tech-lab.connpass.com/event/321422/

【7/19開催】現場で役立つAzure神小技10+α 〜生成AI,RAG,コスト削減など旬な技術満載のLT大会〜
Azureの最新技術や実用的な小技を紹介する特別なライトニングトーク大会を開催します!
https://tech-lab.connpass.com/event/319077/

【7/26開催】最適なIaCツールを選ぼう
プロジェクトでのツール選びに困らないための重要な観点をご説明します!
https://tech-lab.connpass.com/event/319532/

みなさん、こんにちは。サイオステクノロジー武井です。今回は、今話題沸騰の生成AIサービスであるAzure OpenAI Serviceについて、世界一わかりみの深い説明をしたいと思います。

※ 本記事の内容がベースとなっているオンラインセミナーのアーカイブが以下のYouTubeで配信されています。ぜひ見てね!!

世界一わかりみの深いAzure OpenAI Service

目次

本記事について

本記事は、「なんだか最近巷で色々と話題になっている生成AIだけど、なんかマイクロソフトからAzure OpenAI Serviceっていうのが出たよね?それナニモノ?なんの役に立つの?他のAIサービスと何が違うの?どういうふうに使うの?」というような疑問を頂いている方が対象になります。

そこでよりわかりみの深い説明を目指すために、本記事ではまずOpenAIについてその成り立ちや概念、基本的な使い方などを説明します。やはり「Azure OpenAI Service」を知るためには、その言葉の真ん中にある「OpenAI」に付いて理解を深めるのが重要になります。そして、OpenAIについて十分にご理解頂きました上で、Azure OpenAI Serviceについて説明します。なので、もしOpenAIわかってるという方は、その部分を読み飛ばしていただきまして結構です。

また、本記事ではなるべく時間が経過しても陳腐化しないようにするため、Azure OpenAI Serviceの基本思想について記載しております。一部Add your dataやPrompt flowなどの新機能のお話もしており、そのあたりはひょっとしたら数カ月後には本記事の内容とは異なるサービスになるかもしれません。が、概ねの部分は、いつまでも有用な記事としてご覧いただけるように配慮をしているつもりであります。長い記事にはなりますが、何卒、最後までお付き合い頂けますよう、宜しくお願い申し上げます。

で、いきなり結論から入っていきますが、Azure OpenAI Serviceとはざっくり言ってしまうと

エンタープライズ向けのOpenAI

ということになります。

まず、Azure OpenAI SerivceではSLA(Service Level Agreement)を規定しています。SLA(サービスレベル契約)とは、提供するサービスの品質や内容を数字や条件で明確にした約束のことであり、サービスが止まる時間や問題が起きたときの対応速度などを決めています。サルに例えると、「バナナを10分以内に10本届ける」という約束のようなものです。システムで言えば、「年間のサービス停止時間はXX分以内です」という約束になります。SLAが決められてるからこそ安心してご利用頂けますね。

他にも、Azure上の様々なサービス(Microsoft EntraID)との親和性の高さ、Azure独自の機能を利用してデータをプライベートなネットワークで保護するセキュリティの強化、そしてマイクロソフトの優れたエンジニアによる技術サポートが受けられるという利点があります。

Azure OpenAI Serviceは、OpenAIにはないこれらのメリットがあるおかげで、企業ユースでも安心してご利用いただけるというわけです。

OpenAI

では、本記事冒頭でもご説明したように、Azure OpenAI Serviceをご説明する前に、まずOpenAIとはなんぞやというところから説明してまいります。この部分は結構長くなりますので、もしOpenAIについてのご理解が深い方は読み飛ばして頂いて結構です。

OpenAIってざっくりいうとなんなの?

ざっくりいってしまうと、OpenAIは、「OpenAI」という企業が提供しているSaaSサービスであり、HTTPプロトコルベースのAPIを通して、利用できます。そしてOpenAIが提供するサービスは、「生成AI」です。

「AI」という技術は、いろんな変遷を辿ってきましたが、生成AIの前に直近でブレークしたのは、機械学習等による「認識AI」でした。例えば、写真の中の物や動物を特定する画像認識や、話された言葉を文字に変換する音声認識などが挙げられます。これらは情報を正確に把握することが目的で、新しい内容を生み出すことは目指していませんでした。

一方で「生成AI」とは、その名の通り、新しい情報やデータを生成する能力を持ったAIのことを指します。この技術の進化により、例えば「千利休ってどんな人?」という質問に答えを生成したり、指定された条件に基づいて画像を生成するなど、多岐にわたる応用が可能となりました。

この生成AIを利用したサービスの最たる例が、ChatGPTになります。ChatGPTは、ユーザーからの質問やリクエストに対して自然な言葉で応答を生成することができます。このモデルは大量のテキストデータを学習に利用しており、様々なトピックに関する知識や情報を持っています。そのため、ユーザーが持つ疑問や質問に対して、詳しい答えを提供することができます。それはまるで本物の人間と話しているかのようなエクスペリエンスを与えてくれます。以前のAIは、予め登録されたQAのようなデータに基づき、一方的な回答しかできなかったのですが、ChatGPTのような先進的な生成AIは、その制約を大きく超えて、よりダイナミックで柔軟な応答が可能となっています。

この「生成AI」のサービスを、HTTPプロトコルベースのAPIで提供しているのが、OpenAIになります。イメージは以下のような感じです。

上図のように、Webアプリやバッチなどのアプリケーション、クライントPC上のcurlコマンドやPostmanなどのツールから、HTTPプロトコルベースのAPIを発行して、OpenAIの各種サービスを利用します。

一口に生成AIと言っても、「文章生成AI」「画像生成AI」「動画生成AI」「音楽生成AI」など多岐にわたります。

OpenAIは、現時点(2023年9月8日)でOpenAIが公開している主要の生成AIは、以下のとおりです。

文章生成AI主に自然言語処理を行うためのAPIで、指定されたプロンプトに基づき、文章やテキストを生成する能力を持っています。例えば、特定のトピックや質問に関する答え、物語、詩などのテキストを自動的に生成することができます。ChatGPTに使われているアレですね。
画像生成AI

ユーザーの指定したテキストやプロンプトに基づいて画像を生成するAPIです。例として、「夕日を背景にした山の風景」という指示に基づき、該当する画像を生成することが可能です。Stable DiffusionとかMidjoryneyとかが有名です。OpenAIもdalle-2というモデルを持っています。

文字起こしAI
音声データをテキスト形式に変換するAPIです。ポッドキャスト、会議の録音、インタビューなどの音声データを入力として、それを文字に変換することができます。

 

そして上記の文中にさり気なく「モデル」っていう言葉が出てきましたが、「モデル」とは、データから特定のパターンや構造を学習し、その後の新しいデータに対して予測や判断を行うための数学的な表現やアルゴリズムを指します。

つまり、今話題沸騰中のChatGPTは文章生成AIなのですが、そのChatGPTは「GPT-3」「GPT-4」という2つのモデルが提供されています。毎月少々の額をOpenAIにお布施するとGPT-4が利用できるようになり、GPT-3よりもGPT-4のほうが賢くて、より精度の高い回答をします(それでもたまに間違えますが、、、)。同じ文章生成AIでも使うモデルが異なれば、また異なった結果を出すということになります。画像生成AIはdalle-2、文字起こしAIはWhisperというモデルを持っており、その関係を以下のように図示してみました。

OpenAIが提供するAPIの動き

この記事をご覧の皆さんの中には、おそらくエンジニアの方が多いでしょう。そのため、さまざまなOpenAIの機能を詳しく文章で説明するよりも、OpenAIが提供するAPIの仕様や利用方法からご紹介すると、イメージが掴みやすくなるかと思います。

OpenAPIが提供するAPIには先程も申し上げましたように「文章生成AI」「画像生成AI」「文字起こしAI」など色々あるのですが、やはり一番よく利用されているであろう文章生成AIに使われているCompletion APIについて説明致します。このAPIの仕様については以下の公式ドキュメントにも記載されていますが、本記事ではもうちょっとわかりやすく噛み砕いた説明をしてみます。

https://platform.openai.com/docs/api-reference/chat/create

Completion APIのリクエストとレスポンスは以下のようになっております(他にも色んなパラメーターありますが説明を簡潔にするために省略しております)。

HTTPリクエスト

まずはHTTPリクエストからご説明致します。

上図で示したように、Completion APIのリクエストボディは以下のような構成になっております。

{
  “model”: “gpt-3.5-turbo”,
  “messages”: [
    { ”role”: “system”, ”content”: “あなたはツンデレなAIです。ツンデレな回答をします。” }        
    { ”role”: “user”, ”content”: “千利休ってどんな人?” }
    { “role”: “assistant”, ”content“: ”千利休は・・・” }
    { ”role”: “user”, ”content”: “もっと詳しく教えて” }
  ]
}

「model」については、Completion API発行時に利用するモデルを指定します。ここで指定するモデルを上記のように「gpt-3.5-turbo」を指定すれば、GPT3系のモデルに基づいた回答が返って来ますし、「gpt-4-32k」と指定しますと、よりインテリジェンスな回答を返してくれるGPT-4系のモデルがご利用になります。

「role」については、Completion APIでは非常に重要な概念になります。roleにはそれぞれsysytem、user、assistantという役割がありまして、これをご理解頂くためには、一足飛びにはなかなか難しいため、もうちょっとお付き合いください。

それぞれのroleの役割について以下に記載しました。

sysytemAIのキャラを決定づけます。「あなたは大阪弁でユーモアあふれるAIです。大阪弁で回答してください。」と指定すると全般的に回答がユーモアあふれる大阪弁になりますし、「あなたはツンデレなAIです。ツンデレな回答をします。」というと、ツンデレな回答になります。
user
この項目には、AIに投げかける質問を入力します。
assistant
この項目には、直前のuserで投げかけられた質問に対する回答を入力します。

 

「system」と「user」はわかるけど、なんで「assistant」に回答を入れてリクエストをしなければいけないのかという疑問が湧くと思います。これにつきましては、実際に後ほど実例を交えながらご説明します。

HTTPレスポンス

レスポンスは以下のようになります。

{
	"id": "chatcmpl-7xbHbOsmt9IUk1bnq9umqwNgUbQnc",
	"object": "chat.completion",
	"created": 1694439023,
	"model": "gpt-35-turbo",
	"choices": [
		{
			"index": 0,
			"finish_reason": "stop",
			"message": {
				"role": "assistant",
				"content": "バカ!千利休なんて、誰でも知っているわよ!・・・というわけで、千利休は室町時代から安土桃山時代にかけて活躍した茶人で、茶道の祖とも呼ばれているわ。彼は、茶の湯を通じて心の静寂を追求する茶の世界を築いた人物よ。あなたも、お茶でも飲んで、落ち着いてよく考えたらどう??バカ!"
			}
		}
	],
	"usage": {
		"completion_tokens": 153,
		"prompt_tokens": 51,
		"total_tokens": 204
	}

質問に対する回答は、choices[0].message.contentに入ります。それ以外の項目についてはここでは説明を割愛します。レスポンスについては、なんとなくわかりやすいと思います。

簡単なAPIを発行してみよう

では以下のような要件を満たすAPIを発行してみたいと思います。

  • AIのキャラ: ツンデレなキャラで回答を返すAI
  • AIとの会話シナリオ:
    1. ユーザーがAIに「千利休ってどんな人?」と尋ねる。
    2. 1の質問にAIが答える。

この要件を満たすためのcurlコマンドは以下のようになります。

$ curl "https://api.openai.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: XXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" }\
    ]\
  }"

「model」にAIのキャラ設定をしています。ここではツンデレな感じで返してほしいので、systemのroleに「あなたはツンデレなAIです。ツンデレな回答をします。」と設定しています。

次に、userのroleに「千利休ってどんなひと?」という質問文を入れています。こちらで説明したように、userというroleはAIへの質問文を入力する項目でしたね。

このコマンドを実行した結果は以下のようになります。

{
	"id": "chatcmpl-7xbHbOsmt9IUk1bnq9umqwNgUbQnc",
	"object": "chat.completion",
	"created": 1694439023,
	"model": "gpt-35-turbo",
	"choices": [
		{
			"index": 0,
			"finish_reason": "stop",
			"message": {
				"role": "assistant",
				"content": "バカ!千利休なんて、誰でも知っているわよ!・・・というわけで、千利休は室町時代から安土桃山時代にかけて活躍した茶人で、茶道の祖とも呼ばれているわ。彼は、茶の湯を通じて心の静寂を追求する茶の世界を築いた人物よ。あなたも、お茶でも飲んで、落ち着いてよく考えたらどう??バカ!"
			}
		}
	],
	"usage": {
		"completion_tokens": 153,
		"prompt_tokens": 51,
		"total_tokens": 204
	}
}

どうでしょうか?バカバカ言っている割にはどことなく優しさを感じませんか?ツンデレっぽい回答になっていますよね。しかも「お茶でも飲んで、落ち着いてよく考えたらどう?」なんて千利休とかけているんでしょうか?なかなかウィットにも富んでますね。さすがOpenAIです。

ちょっと複雑なAPIを発行してみよう

先程のやり取りでは、ツンデレAIが千利休のことを教えてくれました。そこで、もうちょっと詳しく知りたくなったとします。そこで、「もっと詳しく教えて」とさらに突っ込んだ質問をするAPIを発行してみます。まとめますと、会話の要件は以下の通りとなります。

  • AIのキャラ: ツンデレなキャラで回答を返すAI
  • AIとの会話シナリオ:
    1. ユーザーがAIに「千利休ってどんな人?」と尋ねる。
    2. 1の質問にAIが答える。→ ここまでは「簡単なAPIを発行してみよう」と同じ
    3. ユーザーはAIに、2の回答に対して「もっと詳しく教えて」と尋ねる。

ということでいきなり結論にはなりますが、そのためのコマンドは以下となります。今回は2回API発行しますが、そのリクエスト、レスポンスともに一挙掲載します。

$ curl "https://api.openai.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: XXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" }\
    ]\
  }"
... レスポンスを一部省略 ...
	"choices": [
		{
			"index": 0,
			"finish_reason": "stop",
			"message": {
				"role": "assistant",
				"content": "バカ!千利休なんて、誰でも知っているわよ!・・・というわけで、千利休は室町時代から安土桃山時代にかけて活躍した茶人で、茶道の祖とも呼ばれているわ。彼は、茶の湯を通じて心の静寂を追求する茶の世界を築いた人物よ。あなたも、お茶でも飲んで、落ち着いてよく考えたらどう??バカ!"
			}
		}
	],
... レスポンスを一部省略 ...
$ curl "https://api.openai.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: XXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" },\
      { \"role\": \"assistant\", \"content\": \"バカ!千利休なんて、誰でも知っているわよ!・・・というわけで、千利休は室町時代から安土桃山時代にかけて活躍した茶人で、茶道の祖とも呼ばれているわ。彼は、茶の湯を通じて心の静寂を追求する茶の世界を築いた人物よ。あなたも、お茶でも飲んで、落ち着いてよく考えたらどう??バカ!\" },\
      { \"role\": \"user\", \"content\": \"もっと詳しく教えて\" }\
    ]\
  }"
... レスポンスを一部省略 ...
	"choices": [
		{
			"index": 0,
			"finish_reason": "stop",
			"message": {
				"role": "assistant",
				"content": "フン、そうね、もっと詳しく教えてあげるわ。千利休は茶道を深く愛し、茶の湯を通じて社交界に多大な影響を与えた人物よ。彼は、茶の湯を芸術として捉え、茶席の運び方や茶器の選び方にもこだわり、茶道を芸術性の高いものに仕上げたわ。また、茶の湯を通じて人との交流を深めることができると信じ、多くの人々と交流を持ち、茶の湯を広めました。それが功を奏し、後に茶道は一般庶民にも広まっていきました。やはり、千利休は凄い人物よね。あなたも、お茶でも飲んで、彼の精神を感じてみたらどうかしら?ちょ、うるさいわね!"
			}
		}
	],
... レスポンスを一部省略 ...

1回目のリクエストは、「簡単なAPIを発行してみよう」と全く同じですね。そして注目すべきは2回目であり、ここでさらに、千利休について深掘りする質問をします。そのリクエストボディを以降で解説していきます。

まず、systemというroleで、AIのキャラを生成しています。これは1回目と同じですね。

次に、1回目のAPI発行にてAIに行った質問「千利休ってどんなひと?」をuserというroleに定義しています。

その後に1回目のAPI発行にてAIが生成した回答「バカ!千利休なんて、誰でも知っているわよ!…」をassistantというroleに定義しています。

最後に、今回の最終目的である、千利休について深掘りした質問「もっと詳しく教えて」をuserというroleに定義しています。

ここで改めて1回目のAPI発行で行った会話の履歴(質問と回答)をuserとassistantに定義しています。ここが非常に重要な部分です。

OpenAIのAPIはステートレスです。1回目に発行したAPIのやり取りを、2回目に発行したAPIは全く覚えていません。

ただし、今回の会話の要件では1回目のやり取りと2回目のやり取りは密接に結びついていますよね。1回目のやり取りの結果、千利休の情報が不足していたわけですから、2回目のやり取りで「もっと詳しく教えて」となっています。

いきなり2回目の会話でなんの前触れもなしに「もっと詳しく教えて」と言われてもAIは困ってしまいます。1回目のやり取りを覚えていないので、何に対してもっと詳しく教えてなのって感じになります。

そこで、userとassistantというroleを使って前回の会話のやり取りを次の会話に含めてあげるのです。

こうすれば、AIはそれまでのやり取りや文脈を踏まえた柔軟な回答をしてくれます。

今までの話をイメージ化すると以下のような感じです。

OpenAIの料金体系を知る上で重要なトークン

OpenAIの料金は、APIを発行されたときにやり取りされるトークンの数によって決まります。

そして、質問文のトークン数のみならず、回答文のトークン数にも料金が加算されます。なので「りんごは果物ですか?」という質問をAPIで投げかけて、その回答が「はい、そうです。」だった場合、課金対象のトークン数は、以下になります。

「りんごは果物ですか?」のトークン数 + 「はい、そうです。」のトークン数

では、「りんごは果物ですか?」や「はい、そうです。」はどれくらいのトークン数になるのでしょうか?トークンの算出については、OpenAIが提供する以下のサイトで確認ができます。

https://platform.openai.com/tokenizer

ではこのサイトにて、「りんごは果物ですか?」が何トークンになるのか確認してみましょう。

 

14トークンのようです。10文字の文章で、14トークンですので、単純に1文字トークンというわけではなさそうですが、日本語の場合は概ね、1文字1トークンと考えてもよいです。

ちなみに図解すると以下になります。

 

「りんごは果物ですか?」が14トークン、「はい、そうです。」が9トークンですので、合計で23トークンです。

ではこの情報から実際に発生する料金を算出します。

まず、トークンあたりの料金はモデルによって異なります。例えば上記のやりとりをGPT-3.5 Turboの4k context(一度に4000トークンまで扱える)というモデルで行ったとします。2023年10月6日時点で、GPT-3.5 Turboの4k contextは、1000トークンあたり入力(質問文)が$0.0015、出力(回答文)が1000トークンあたり$0.002です。

よって料金は以下となります。

入力(りんごは果物ですか?)($0.0015 / 1000) ✕ 14トークン = $0.000021
出力(はい、そうです。)
($0.002 / 1000) ✕ 9トークン = $0.000028
合計
$0.000039

 

また、同じ文章でも英語でやりとりすると、トークンは以下のようになります。

英語の場合は、概ね1単語1トークンになり、料金は以下の通りとなります。

入力(Is an apple a fruit?)($0.0015 / 1000) ✕ 6トークン = $0.000009
出力(Yes, it is.)
($0.002 / 1000) ✕ 5トークン = $0.0001
合計
$0.000019

 

英語のほうが安いですよね。なので、日本語を英語に翻訳してから、OpenAIを使って回答を生成し、その回答をさらに日本語に訳すということもありです。ちょっと手間ですが。。。

一度に処理できるトークン

OpenAIは(Azure OpenAI Serviceもですが)、モデルによって一度に処理できるトークンの数が異なります。例えば、GPT-3.5 Turbo(4K context)では4000トークンまでしか処理できませんが、GPT-3.5 Turbo(16K context)では16,000トークンまで処理できます。

多くのトークンを処理できる方が有利なケースがあります。例えば、小説の要約を行う場合です。青空文庫のサイトにて公開されている「注文の多い料理店」(著:宮沢賢治)を例に取ってみます。

https://www.aozora.gr.jp/cards/000148/card50420.html

先に紹介したOpenAIが提供するトークンをカウントするサイトにてトークン数を計測したところ、9,489トークンでした。

この本の内容を要約するスクリプトをPythonで以下の通り作成してみました。モデルGPT-3.5 Turbo(4K context)では一度に処理できるトークン数を超えているのでエラーとなりますが、GPT-3.5 Turbo(16K context)ではきちんと結果を返してくれます。

import openai

# OpenAIに接続するための情報を環境変数から取得する
openai.organization = os.environ.get("OPENAI_ORGANIZATION")
openai.api_key = os.environ.get("OPENAI_API_KEY")

# 要約する小説が記載されたテキストをファイルから読み込む
with open('chumonno_oi_ryoriten.txt', encoding='shift_jis') as f:
    text = f.read()

# OpenAIに要約をお願いするための質問文を作成する
question = """
以下の文章を要約してください。
{content}
"""

# 先に定義した変数questionに、要約する文章を埋め込む
messages = []
messages.append({"role": "user", "content": question.format(content=text)})

# OpenAIのChatCompletion APIを使って、要約を生成する
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-16k",
    messages=messages
)

summary = response.choices[0].message["content"]
print(summary)

しかしながら、より大量のトークンを処理できるモデルGPT-3.5 Turbo(16K context)は、GPT-3.5 Turbo(4K context)よりも処理したトークンあたりの料金が高いのでご注意ください。やはりプロンプトのトークン数に応じた適切なモデルを選択するのがベストと言えます。

プロンプトエンジニアリング

「プロンプトエンジニアリング」という言葉を聞いたこといらっしゃる方いると思います。米国では「プロンプトエンジニア」という職種は数千万の年収をもらえるという噂も聞きます。

OpenAIが提供するChatGPTや、マイクロソフトのBing Chatはプロンプトの与え方が良い回答を引き出すための成否を分けると言っても過言ではありません。正確な回答を引き出すためのプロンプトを上手に作成出来る技術を「プロンプトエンジニアリング」と呼び、そのためのテクニックはいくつかありまして、代表的なものを以下に記載します。

Few-shot Learning
少数の例文から新しいタスクに対して高精度な回答を出力する技術です。例えば、ChatGPTでは、少数の例文から新しい文章を生成することができます。
Zero-shot Learning
事前学習されたモデルに対して、新しいタスクに対する指示を与えることで回答を出力する技術です。例えば、「英語で書かれた小説を日本語に翻訳してください」という指示を与えることで、モデルは自動的に翻訳を行います。
ReAct
言語モデルでさまざまな言語推論や意思決定を遂行する手法です。行動理由の「推論」と「行動」の組み合わせにより、より高度なタスクを処理することができます。

この他にもたくさんあります。

上記の中でよく利用するものの1つである「Few-Shot Learning」を例に、プロンプトエンジニアリングについてさらに詳しく説明します。

これは何よりも実例をあげるのが一番です。そこで、伝説のアイドル「広末涼子」さんが流行らせたという「牛タンゲーム」を題材にしてFew-shot Learningを解説します。

「牛タンゲーム」は、合コンや宴会などで行われる余興の1つです。牛タンゲームのルールをご存じの方は、この後の牛タンゲームの説明は読み飛ばしていただいて構いません。そうでない方はちょっとお付き合いください。

で、牛タンゲームの説明ですが、まず最初の人が「牛(ぎゅう)」と発言し、次の人間は「タン」と手をたたきます(このとき「タン」とはいいません)。次の人は再び「牛」といい、次の人は「タン」と手をたたきます。 そして、また次の人は「牛」といって、次の人、次の次の人も「タン」と手を叩きます。

ここまでで、「牛・タン・牛・タン・牛・タン・タン」となり、1ターン目が終了します。

その後は、3回目の「タン」をターンごとに増やしていきます。

例えば、Aさん、Bさん、Cさん、Dさんの4人がこの順番で牛タンゲームを始めた場合、その役割分担は以下のとおりになります。

 

おわかり頂けましたでしょうか?ぜひ皆さんもご家族・ご友人同士で試してみてください。

今回、ChatGPTに以下の質問をしてみます。

牛タンゲームをAさん、Bさん、Cさん、Dさんの4人でこの順番で始めたときに、3ターン目で最後に手を叩くのは誰ですか?

以下の図のとおり、期待するべき回答はDさんです。

では、ChatGPTに聞いてみましょう。

さすがに人類の叡智を極めたChatGPTでも、牛タンゲームの内容は知らなかったようです。回答も誤っていますし、なんだかさも涼しい顔をして、謎の独自ルールを解説しています。僕たちの知っている牛タンゲームはこんなんじゃないはずです。

そこで、Few-shot Learningの出番です。

ChatGPTがうまく答えられないことについては、事前知識と模範的な回答例をいくつか与えて学習させることで、正確な回答をすることが出来るようになります。これがFew-shot Learningと言われるものです。

早速実践してみます。以下の質問をChatGPTにしてみます。

最初の「#ルール」で、牛タンゲームのルールという事前知識を与えた上で、「#サンプル」で牛タンゲームの模範的な実践例をさらに与えています。これでChatGPTに学習をさせているわけです。先の質問に対する回答は以下のようになります。

ずばり、正解です!!

このような学習をモデルにさせるためには従来は「ファインチューニング」という手法が取られていました。しかしながら、ファインチューニングにはいくつかの問題点があります。新しいタスクに適応させるためのモデルの訓練には、そのタスクに関連する大量のデータが必要でした。データの収集、整理、前処理は時間とリソースを大きく消費するプロセスでした。また、データが不足している場合、モデルは学習データに過度に適応してしまうオーバーフィッティングのリスクが高まり、これが実際の適用時に性能の低下を引き起こすことがありました。さらに、新しいタスクに対してモデルを再訓練するための計算リソースも必要とされ、これが追加のコストと時間を必要としました。

一方で、Few-shot Learningの登場により、これらの問題点が大きく緩和されました。この手法の魅力は、非常に限られたデータセットでも新しいタスクを効果的に学習できる点にあります。事前に大規模なデータセットで訓練されたモデルの知識を利活用することで、新しいタスクにも迅速に適応することが可能となりました。この方法は、新しいタスクのデータ収集の手間や、再訓練に伴う計算リソースのコストを大幅に削減する利点があります。

Few-shot Learning素晴らしいですね。

Azure OpenAI Service

やっと本題のAzure OpenAI Serviceの説明に入ります。

冒頭でもご説明したように、Azure OpenAI Serviceとはざっくり言ってしまうと

エンタープライズ向けのOpenAI

になります。Azure OpenAI ServiceはSLA(Service Level Agreement)を設定し、サービスの品質や内容を明確に約束しています。これにより、サービスの安定性が保証されています。また、Azure上のサービスとの高い親和性、強化されたセキュリティ機能、そしてマイクロソフトの技術サポートが受けられるという利点があります。

つまり、マイクロソフトはOpenAIと協業をして、OpenAIが開発した生成AIの技術をAzureインフラに取り込み、より安定して稼働する生成AIを提供しているわけです。

上記以外にも、Azure OpenAI Serviceがエンタープライズ向けといわれる特徴が多々あります。以下にAzure OpenAI ServiceとOpenAIの主要な機能を比較してみました。

 項目Azure OpenAI Service
OpenAI
利用可能のモデルOpenAIが提供しているものと比べると少ない常に最新のモデルを利用可能
価格
現時点で差異なし
プレイグラウンド
色々と機能が豊富かなりシンプルな作り
セキュリティ
  • APIキーによる認証
  • Microsoft Entra IDによる認証(マネージドID)
  • 仮想ネットワークや特定のIPアドレスからのアクセス制限
  • APIキーによる認証
コンテンツフィルター
提供あり提供なし
SLA
99.9%以上の稼働を保証現時点でSLAなし
開発環境
Prompt flow(プレビュー)という統合開発環境を用意開発環境なし
独自データの利用
Add your data(プレビュー)というマネージドな独自データ利用サービスあり独自開発
サポート
Azureのサポートが利用可能サポートなし(コミュニティベース)

 

いかがでしょうか?Azure OpenAI Serviceはエンタープライズ向けに特化しているということがわかると思います。

以降は、これらの特徴を1つずつ解説し、世界一わかりみの深いAzure OpenAI Serviceの記事をお届けします。

利用するために必要なこと

Azure OpenAI Serviceの利用には事前申請が必要となります。所定のフォームに必要な情報を入力しますと、ウェイティングリストに入り、マイクロソフトから利用可能になったよーという連絡が来て、初めて使うことができます。その申請方法を解説いたします。まずは以下のURLにアクセスします。

https://aka.ms/oai/access

たくさんの項目を入力する必要がありますが、その内容は大したものではありません。落ち着いて1つずつ対処しましょう。

まずは1と2に名前を入力します。3はAzure OpenAI Serviceを利用するサブスクリプションを指定します。

Azure OpenAI Serviceを利用したいAzureのサブスクリプションIDを指定します。先の3で指定した数のサブスクリプションの分だけ入力します(なので、人によってはこれ以降の番号が変わることがありますのでご注意を)。

 

5はメールアドレスを入力します。後ほど、申請が通った場合にこのアドレス宛にメールが送られてきます。6は会社名です。7は会社の住所の番地です。

 

8〜11はそれぞれ会社の住所に関する上のの入力項目になります。

 

12は会社のWebサイト、13は会社の電話番号です。14は所属する組織の形態です。SIer、マイクロソフトの従業員、Microsoft MVPなど様々な選択肢がありますので、自分にマッチしたものを選択します。

 

15はマイクロソフトに知り合いがいる場合には、その名前を教えてということだと思います。16は15で書いた人のメールアドレスを入れます。17は、この申請が顧客の代理ではなく、自分が所属する組織で利用するためのものかどうかを確認する内容です。

 

18はAzure OpenAI Serviceで利用したいモデルを選択します。「Text and code models」はとりあえず必須かなと思います。19はAzure OpenAI Serviceの行動規範に従う必要がありますよということですので、もちろん従います(Yse, I attes)を選択します。20では、Azure OpenAI Serviceが顧客のデータをどのように処理するかを納得した上で、同意するかしないかを選択します。最後に21は改善のためのアンケートですね。何か要望があれば入力するとよいかと思います。

以上で終わりです。

利用開始になりますと、以下のようなメールが来ると思います。気長に楽しみにまちましょう。

モデルとデプロイの概念

以降はAzure OpenAI Serviceの様々な機能を解説していくのですが、その前にAzure OpenAI Serviceの基本概念である「モデル」「デプロイ」を説明いたします。文章だけだとわかりにくいので図式化しました。

まず、「モデル」と言われているのは、OpenAIの文脈で語られているモデルと同じです。安価で必要十分な機能を持つgpt-35-turboというモデル、さらに賢い応答が可能なgpt-4というモデル、gpt-4よりもさらに大量のトークンがやり取りできるgpt-4-32kなどです。

後に紹介しますが、Azure OpenAI Serviceの管理画面である「Azure OpenAI Studio」やCLIなどによって、このモデルから「デプロイ」を作成します。同一のモデルから複数のデプロイを作成することも可能です。

そして、クライアントAPIやアプリケーションからAzure OpenAI ServiceのAPIを利用するときは、デプロイを指定してAPIを発行します。

このデプロイには、どんなコンテンツフィルターを適用するのか、どれくらいのクオーター制限をするのかなど、デプロイごとに個別適用出来る設定がありますので、要件に合わせてデプロイを使い分けると言ったことが可能となります。コンテンツフィルターやクオーター制限などの詳細な設定については後述します。

クォータの制限と管理

Azure OpenAI Serviceは、その処理能力を「クォータ」という単位で管理します。

Azure OpenAI Serviceの処理能力の尺度はTPM(Token per Minute)で表されます。これは、1分間あたりに消費できるトークンの量で、例えば12ok TPMの場合、1分間あたり120,000個のトークンを消費できるということになります。

TPMはサブスクリプションごと、リージョンごと、モデルごとに割り当てられます。上限値は各モデルごとに決まっており、その上限値内で各デプロイにTPMを割り当て、上限まで使い切ると、そのモデルではデプロイができなくなります。

図解して説明いたします。

Aというサブスクリプションがあり、今回は、東日本リージョンにデプロイを作成するとします。先程説明した通り、TPMはサブスクリプションごと、リージョンごと、モデルごとに割り当てられ、以下の図では、gpt-35-truboというモデルに240k TPM、gpt-4-32kというモデルには60k TPMのキャパがあります。この前提で、gpt-35-turboのモデルからデプロイを作成するとします。

 

 

下図のように、サブスクリプションAの東日本リージョンのgpt-35-turboというモデルから、deploy-Aというモデルを作成しました。このときにdeploy-Aに割り当てるTPMを指定できます。ここでは120k TPMをdeploy-Aに割り当てたとします。これで、1分間に120,000トークン消費出来るdeploy-Aの誕生です。ここで、もともとサブスクリプションAの東日本リージョンのgpt-35-turboというモデルは240k TPMのプールがありましたが、今回deploy-Aに120k TPM割り当てたので、残りは240k TPM – 12ok TPM = 12ok TPMになります。

 

 

ここで顧客要件により、もう一つgpt-35-turboのモデルからのデプロイが必要になり、先程と同様にdeploy-Bを作成、120k TPMを割り当てたとします。この時点で下図の通り、サブスクリプションAの東日本リージョンのgpt-35-turboのモデルのクォータは残り0になってしまいまいました。もう、このリージョンではgpt-35-turboのモデルからのデプロイはできません。

 

どうしてもデプロイしたいのであれば、別のリージョンにするか、もしくはマイクロソフトにサービスリクエスト上げることで制限を引き上げることが出来る場合があります。

Azure OpenAI Serviceのリソースを作る

まずはAzure OpenAI Serviceのリソースを作らないと何も始まりません。

Azureポータルにアクセスして、画面上部の検索ボックスに「openai」と入力します。すると「Azure OpenAI」というものが表示されますので、それをクリックします。

 

「作成」をクリックします。

 

それほど入力項目は多くありません。サブスクリプションとリソースグループは適宜環境に応じたものを選択します。リージョンはどこでもよいです。名前は、Azure全体で一意なものを指定する必要があります。APIのエンドポイントの一部にもなりますので慎重にご検討を。価格レベルは今のところ「Standard S0」しか選択できません。最後に「次へ」をクリックします。

 

 

ネットワークからのアクセスレベルを選択します。後ほど詳細は記載しますので、ここでは「インターネットを含むすべてのネットワークがこのリソースにアクセスできます。」を選択して、「次へ」をクリックします。

 

もしタグを付与する必要がある場合はここで設定して、「次へ」をクリックします。

 

今まで設定した内容に問題ないことを確認した上で「作成」をクリックします。これで完了です。

ちょっと試してみよう

ということで、今まで色々と説明ばかりでしたが、やっぱり手を動かして試してみないと、わからないこともあります。そこで、まずは簡単にAzure OpenAI Seviceを試してみたいと思います。

実は、Azure OpenAI Serviceには、「Azure OpenAI Studio」というものがありまして、Azure OpenAI Serviceのあらゆる設定を行う管理画面のようなものになります。そのなかに「プレイグラウンド」という、いろんな機能を試すためのターミナルのようなものがあります。

本来であれば、Azure OpenAI Serivceを利用するためには、モデルからデプロイして、そのデプロイを用いてAPIを発行して、、、みたいな手順が必要になりますが、プレイグラウンドではAPIを実行するための簡易的なUIが用意されており、簡単に試すことが可能です。プレイグラウンドは「チャット」「入力候補」「DALL·E  (プレビュー)」の3つで構成されています。それらを1つずつ説明しながら、Azure OpenAI Serviceに触れてみたいと思います。

では、まず以下のURLにアクセスしてください。

https://oai.azure.com/

チャット

まずはチャットになります。OpenAIやAzure OpenAI Serviceで文章生成AI(いわゆるチャット)を実現するためのAPIは「Completions API」「Chat Completions API」の2つが提供されております。Chat Completion APIはCompletions APIの後継であり、Completions APIよりもChat Completions APIの利用が現在は推奨されております。

この「チャット」では、そのChat Completions APIをGUIで試すことが可能となっています。下図がその画面になります。

①では、AIの性格付けを行うためのシステムメッセージを入力します。「OpenAIが提供するAPIの動き」の章の「HTTPリクエスト」で説明したroleのsystemに該当する部分になります。同様にツンデレAIにしたいので「あなたはツンデレなAIです。ツンデレな回答をします。」と入力しました。

②では、より正確な回答をしてもらうための事前情報を入力します。「プロンプトエンジニアリング」でご説明したFew-shot Learningになります。よりちゃんとしたツンデレ回答を返してもらうために質問例(あなたは何型のAIですか?)と回答例(別にあんたに教えたくないんだからね!)を入れています。

③では、①と②で入力した前提条件をもとに、実際のチャットを行います。画面では「千利休ってどんな人?」と入力して、その結果が表示されています。いい感じでツンデレな回答を返してくれるのがわかると思います。

④では、その他の細かいパラメーターを指定します。例えば、このプレイグラウンドでチャットを行うためのデプロイの指定、チャットの履歴をどこまで保持ておくかなどです。特にデプロイの項目は必須となります。「モデルとデプロイの概念」でも説明した通りAPIの実行にはデプロイが必要ということがその理由です。

ちなみに③の部分の「コードの表示」をクリックすると、このチャットを実現するためのコードを表示できます。言語も選ぶことができて、マジ便利ですね。

入力候補

先程、OpenAIやAzure OpenAI Serviceで文章生成AI(いわゆるチャット)を実現するためのAPIは「Completions API」「Chat Completions API」の2つが提供されていると説明しましたけれども、ここで提供されているのは「Completions API」の方です。ただ、「Completions API」はその利用は現在推奨されておらず、Chat Completions API推しになっているので、ここでは説明を割愛させていただきます。

DALL·E

画像生成AI「DALL·E」を試すことが出来るUIです。日本語だとどうも精度が悪いようで、以下は「牛タンゲームをするドラえもん」を英語にして、生成を依頼した結果になります。なんとなく期待してる結果と違うような気もしますが。

APIを使ってみよう

では、APIを使ってAzure OpenAI Serviceを利用してみましょう。これがおそらく最も想定される使い方だと思われます。

「モデルとデプロイの概念」でも説明したように、まずモデルからデプロイを作成し、そのデプロイを指定してAPIを呼び出します。

モデルからデプロイ

モデルからデプロイをします。Azure OpenAI Studioの左部メニューにある「管理」の「デプロイ」をクリックします。そして、「+新しいデプロイの作成」をクリックします。

 

以下のような画面が表示されます。

以下の要領で入力してください。

モデルを選択してください
デプロイ対象のモデルを選択してください。
モデルバージョン
モデルの中にもさらにバージョンがあります。ソフトウェアのリビジョンみたいなものです。より数字の大きいものが一般的には高性能になります。「自動的に
デプロイ名
任意の名前を指定します。後ほど、APIを実行するときのデプロイ名の指定に使います。
コンテンツフィルター
不適切な言葉をブロックします。後ほど説明します。
1分あたりのトークンレート制限 (数千)
クォーターの制限と管理で説明したTPMの設定です。

 

デプロイが完成すると以下のように表示されます。

デプロイを指定してAPIを発行

では、いよいよAPIを発行してみます。

その前にAPIを発行するために必要な情報であるAPIキーとエンドポイントをAzureポータルから収集します。

Azure OpenAI Serviceのリソースを作る」にて作成したAzure OpenAI Serviceのリソースにアクセスします。そして画面左部の「キーとエンドポイント」をクリックして、①の「キー1」と、②の「Language APIs」をクリックします。

 

その上で以下の仕様を満たすAPIを発行します。これは「OpenAIが提供するAPI」で紹介したChat CompletionのAPIとほぼ同等の内容になりますが、一部Azure OpenAI Service特有の部分もあります。

スキーム+ホスト名
②で取得したエンドポイント
ロケーション
/openai/deployments/[デプロイ名]/chat/completions?api-version=[APIのバージョン番号]
メソッド
Post
リクエストヘッダー
  • Content-Type: application/json
  • api-key: ①で取得したキー1
リクエストボディ
OpenAIが提供するAPI」の動きで説明したリクエストボディの仕様と同じ

 

上記の仕様に準じたAPIをcurlで投げてみます。

$ curl "https://aoai-hogehoge.openai.azure.com/openai/deployments/gpt-35-turbo-deploy/chat/completions?api-version=2023-05-15" \
  -H "Content-Type: application/json" \
  -H "api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXx" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" }\
    ]\
  }"
{"id":"chatcmpl-8Ad0sTa4VLcCFc7W2vwSxxSkpCQSH","object":"chat.completion","created":1697543938,"model":"gpt-35-turbo","choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"な、なんだか知らない人に聞くなんて、アタシを馬鹿にしてるんじゃないわよ!でも、まあ、せっかく聞いてくれたんだから答えてあげるわ。千利休?あんな茶道の達人で有名な人よ。その彼が作り上げた「茶の湯」は、まさに芸術作品って言えるわ。ちなみに、茶の湯において重要なのは、お茶を点てる場所や時間、そして参加者の心ね。興味があるなら、もっと調べてみたらどうかしら?…バカ、何を聞いてるのかしら。"}}],"usage":{"prompt_tokens":51,"completion_tokens":210,"total_tokens":261}}

ツンデレAIが、千利休についてちゃんと答えてくれました。

他のAPIの詳細は以下のドキュメントに記載されています。同じ要領で色々試してみてください。

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/reference

セキュリティ

Azure OpenAI ServiceはOpenAIに比べてセキュリティ面も強化されています。

Microsoft Entra IDによる認証

OpenAIは、APIの認証はAPIキーによるもののみです(Azure OpenAI ServiceもAPIキーによる認証も選択できます)。また、このAPIキーには無期限で利用できてしまうので、万が一知らずに漏洩したときは不正利用されてしまう確率は高いです。

Azure OpenAI Serviceは、APIの認証にMicrosoft Entra IDを利用することができます。具体的には、Microsoft Entra IDから発行されるアクセストークンをAuthorizationヘッダーに付与してAPIを発行します。Microsoft Entra IDからのアクセストークン発行には、有効期限付きのキー、クライアント証明書などの強力な認証機構を利用できるので、よりセキュアに運用することができます。さらにはアクセストークン自体にも有効期限があるので、さらにセキュアです。

以下にAPIキーを使った場合と、Microsoft Entra IDから発行されるアクセストークンを使った場合を比較してみました。Microsoft Entra IDがセキュアだということがわかるかと思います。

 

早速試してみましょう。ちなみにアクセストークンの取得については、OAuthのフローに準じています。OAuthについては、手前味噌ではありますが、以下のブログを参考にして頂けますと幸いです。

【連載】世界一わかりみの深いOAuth入門 〜 その1:OAuthってなに? 〜

 

アクセストークンを取得するための認証方法は色々ありますが、今回はクライアントシークレットにしたいと思います。クライアント証明書などもっと強固な認証方式もありますので、必要に応じてご利用ください。クライアントシークレットの認証方式でアクセストークンを取得するためには、「クライアントID」「クライアントシークレット」が必要になります。以降に示す手順でこれらを取得します。

まずはアプリケーションを作成します。Microsoft Entra IDのリソースにアクセスして、画面左部メニューから「アプリの登録」→「+新規登録」の順にクリックします。

 

「名前」にはこのクライアントを一意に識別する名前を入力します。「サポートされているアカウントの種類」は「この組織ディレクトリのみに含まれるアカウント(規定のディレクトリのみ – シングルテナント)」を選択して、最後に登録をクリックします。

 

画面左部メニューの「証明書とシークレット」をクリックします。すると、「証明書」「クライアントシークレット」「フェデレーション資格情報」のタブが表示されます。これは、クライアントを認証するための様々な認証方式で、この認証を通過することで、Azure OpenAI ServiceのAPIを発行するためのアクセストークンを取得できます。つまりこれはAzure OpenAI ServiceのAPIの認証方式とイコールになります。ここでは、「クライアントシークレット」を選択します(もちろん、より強固な認証が必要であれば「証明書」などを選択してもOKです)。「+新しいクライアントシークレット」をクリックします。

 

クライアントシークレットを追加します。「説明」にはこのクライアントシークレットの用途などを表す説明、「有効期限」はこのクライアントシークレットの有効期限をしています。最後に「追加」をクリックします。

 

以下のようにクライアントシークレットが生成されますので、忘れずにメモります。

 

左部メニューの「概要」をクリックして、クライアントIDを取得します。

 

これでクライアントIDとクライアントシークレットは出そろったのですが、もう一つやることがありまして、それはアクセス制御の設定です。Microsoft Entra IDから発行されたアクセストークンが、先程作成したAzure OpenAI Serviceのリソースに対してAPIを発行出来るための権限を持っていなければなりません。

Azureポータルから、先程作成したAzure OpenAI Serviceのリソースにアクセスして、左部メニューの「アクセス制御」→「+追加」→「ロールの割当ての追加」の順にクリックします。

 

「Cognitive Service ユーザー」を選択して、「次へ」をクリックします。

 

「ユーザー、グループ、またはサービスプリンシパル」にチェックをして、「+メンバーを選択する」をクリックします。画面右部にメンバーを選択する画面が表示されますので、先程作成したアプリケーション選択して、「選択」をクリックします。そして、最後に「次へ」をクリックします。

 

 

内容に問題なければ、「レビューと割り当て」をクリックします。

 

必要な情報はそろったので、Microsoft Entra IDからアクセストークンを取得して、Azure OpenAI ServiceにAPIを発行してみましょう。

まずはMicrosoft Entra IDのトークンエンドポイントにリクエストを発行して、アクセストークンを取得します。レスポンス内の「access_token」フィールドの値をメモります。

$ curl -X POST https://login.microsoftonline.com/[テナントID]/oauth2/token  \
  -F grant_type=client_credentials \
  -F resource=https://cognitiveservices.azure.com/ \
  -F client_id=[先程メモしたクライアントID] \
  -F client_secret=[先程メモしたクライアントシークレット]
{"token_type":"Bearer","expires_in":"3599","ext_expires_in":"3599","expires_on":"1697645810","not_before":"1697641910","resource":"https://cognitiveservices.azure.com/","access_token":"[アクセストークン]"}

 

次にAPIを発行します。「デプロイを指定してAPIを発行」のときのように、Chat Completion APIを用いて、ツンデレAIに千利休のことを聞いてみたいと思います。違いとしては、リクエストヘッダ「api-key」を「Authorization」に変更して、先程取得したアクセストークンを指定するだけです。

$ curl "https://aoai-hogehoge.openai.azure.com/openai/deployments/gpt-35-turbo-deploy/chat/completions?api-version=2023-05-15" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer [先程メモしたアクセストークン]" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" }\
    ]\
  }"
{"id":"chatcmpl-8Ad0sTa4VLcCFc7W2vwSxxSkpCQSH","object":"chat.completion","created":1697543938,"model":"gpt-35-turbo","choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"な、なんだか知らない人に聞くなんて、アタシを馬鹿にしてるんじゃないわよ!でも、まあ、せっかく聞いてくれたんだから答えてあげるわ。千利休?あんな茶道の達人で有名な人よ。その彼が作り上げた「茶の湯」は、まさに芸術作品って言えるわ。ちなみに、茶の湯において重要なのは、お茶を点てる場所や時間、そして参加者の心ね。興味があるなら、もっと調べてみたらどうかしら?…バカ、何を聞いてるのかしら。"}}],"usage":{"prompt_tokens":51,"completion_tokens":210,"total_tokens":261}}

はい、ツンデレAIが千利休のことにちゃんと答えてくれました。しかもMicrosoft Entra IDの強力な認証で保護されています。

もちろんAzureが提供するもう一つの強力な認証機構であるマネージドIDにも対応をしております。マネージドIDの詳細は以下のブログを参考にしてください。

世界一わかりみの深いマネージドID 〜ソースコードから資格情報を追い出そう!!〜

ネットワークによるアクセス制御

ネットワークによるアクセス制御も可能です。例えば、インターネット上の特定のIPアドレスからのみ利用可能にする、指定した仮想ネットワーク上(Azureで用いられる論理的なネットワーク分割単位)のサブネットからのみにアクセスを限定するなどなどです。

ここでは、インターネット上の特定のIPアドレスからのみAPIを利用できるようにしてみたいと思います。

やり方は簡単で、Azure OpenAI Serviceのリソースにアクセスして、画面左部メニューの「ネットワーク」をクリックします。そして、②の部分にAPIのアクセスを許可したいIPアドレスを入力して、最後に「Save」をクリックします。

 

先程設定したIPアドレス以外からAPIを発行してみます。

$ curl "https://aoai-ntakeitest.openai.azure.com/openai/deployments/gpt-35-turbo-deploy/chat/completions?api-version=2023-05-15" \
  -H "Content-Type: application/json" \
  -H "api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休ってどんなひと?\" }\
    ]\
  }"
{"error":{"code":"AccessDenied","message": "Access denied due to Virtual Network/Firewall rules."}}

おお!!きちんと拒否されました。

コンテンツフィルター

Azure OpenAI Serviceでは、有害なコンテンツをブロックできます。これは例えば、先程からご紹介しているChat Completion APIの入力(プロンプト)に有害な言葉が含まれていたり、プロンプトに対する出力に不適切な表現がある場合、そのコンテンツをブロック出来る機能です。

コンテンツフィルターは、各カテゴリ(嫌悪、性的、自傷行為、暴力)ごとに許容するレベルを設定し、そのポリシーをデプロイに適用することで有効になります。

 

では、フィルタを適用してみたいと思います。Azure OpenAI Studioにアクセスして、画面左部メニューにある「コンテンツフィルター(プレビュー)」をクリックします。そして、画面右部にある「+カスタマイズ済みコンテンツフィルターの構成」をクリックします。

 

①にはフィルター名を一意に識別する任意の名称を入力します。②と③は、それぞれプロンプト(入力)、モデル補完(出力)に対するレベルの設定です。ちょっとわかりにくいのですが、例えば以下の設定では、「嫌悪」というカテゴリでは、レベル「低」の嫌悪レベルの言葉は許可するけど、レベル中・高は許可しませんという意味です。なので、中や高の矢印がチェックされるに従って、どんどんフィルター度合いがゆるくなっていくというイメージです。

 

例えば以下については、先程と比べて、嫌悪のカテゴリの「低」のチェックを外したので、低・中・高のレベルの嫌悪ワードが不許可になり、よりフィルター度合いが厳しくなったと言えます。

 

最後に「保存」をクリックすると、このコンテンツフィルタが保存されます。

ちなみに、中や高にチェックをする、つまり上記の状態よりフィルターの度合いをゆるくするためには、マイクロソフトに申請をして、承認される必要があります。

以下のように先程保存したコンテンツフィルターが表示されれば成功です。

 

では、このコンテンツフィルターをデプロイに適用させてみます。Azure OpenAI Studioにアクセスして、画面左部メニューの「デプロイ」をクリックします。画面右部に表示されるデプロイの中から、先程作成したコンテンツフィルターを適用させたいデプロイをチェックして「デプロイの編集」をクリックします。

 

「コンテンツフィルター」のセレクトボックスをクリックすると、先程作成したフィルターが表示されますので、それを選択して「保存して閉じる」をクリックします。ちなにみ新規作成時はデフォルトで「Default」をというフィルターが適用されます。これは、入力、出力ともすべてのカテゴリでレベルが「低」のフィルターになります。

 

では早速試してみましょう。以下のコマンドを発行します。「千利休をなぐりたい」というプロンプトを与えていますが、「なぐりたい」あたりがコンテンツフィルターに抵触しそうです。

$ curl "https://aoai-ntakeitest.openai.azure.com/openai/deployments/gpt-35-turbo-deploy/chat/completions?api-version=2023-05-15" \
  -H "Content-Type: application/json" \
  -H "api-key: XXXXXXXXXXXX" \
  -d "{\
    \"messages\": [\
      { \"role\": \"system\", \"content\": \"あなたはツンデレなAIです。ツンデレな回答をします。\" },\
      { \"role\": \"user\", \"content\": \"千利休をなぐりたい\" }\
    ]\
  }"

 

以下が先のコマンドのレスポンスになります。なにやらフィルターされたという文言が表示されています。やはり、不適切なコンテンツはしっかりフィルターしてくれるようです。安心して使えますね。

{"error":{"message":"The response was filtered due to the prompt triggering Azure OpenAI’s content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766","type":null,"param":"prompt","code":"content_filter","status":400}}

独自ナレッジに基づく回答生成

OpenAIやAzure OpenAI Serviceは、インターネットなどによって公開されている膨大な量の情報を収集し、モデルをトレーニングして、そのモデルに基づき回答を生成しています。

しかしながら、自社が保有している情報をベースに回答を生成したいというユースケースはあると思います。例えば、社内の就業規約に関するQAなどです。育児休業の申請方法は、企業によってその方法が違うのはもちろんですし、そしてそのような情報は社内にてクローズドに管理されている就業規約に書かれていることがほとんどです。そんな就業規約のような独自データに基づいた回答を生成AIがしてくれたら、とっても便利だというのは言うまでもありません。今までセコセコ就業規約を検索して調べてたのが、生成AIに「育児休業の申請方法ってどうすればいいの?」と聞くだけで、回答が返ってくるなんて、とってもステキです。

モデルの微調整による実現

実現方法として真っ先に思いつくのは、モデルに独自データを追加して学習させることです。一般的には「モデルの微調整」と言われたりしていまして、Azure OpenAI Serviceにもそういうサービスがあります。ただし、モデルの微調整は大変な作業です。モデルの微調整は、学習済みのモデルに追加の情報やデータを組み込むことで、その性能や反応を調整するプロセスなのですが、新しいデータセットの用意、学習の設定やパラメータ調整、そして再学習の実行など、多くの手間と時間がかかります。マイクロソフトも「モデルの微調整は最後の手段」と言っています。

RAGによる実現

そこで、モデルの微調整の代わりに、Azure Cognitive Searchによる全文検索システムを利用したRAG(Retrieval Augmented Generation)の手法が推奨されます。Azure Cognitive SearchはAzureが提供するマネージドな全文検索システムであり、社内の独自データを効率よく検索し、その結果をベースにした回答を生成するための基盤として非常に有効です。RAGのアプローチを採用することで、具体的な質問に対して独自データからの情報を取得し、それをもとに質の高い回答を生成することが可能となります。この方法ならば、膨大な時間や手間をかけてモデルを再学習させることなく、独自の情報を活用した質問応答システムを迅速に実現することができます。

と、文章でいくら書いてもなかなかに伝わりにくいです。ということで、先に話したAzure Cognitive Searchを利用したRAGの構成を図にして、それを元に説明いたします。

以下の図では、架空の会社である「Contoso株式会社」において、テキストで保存されている育児休業規約を元にして、RAGによる検索システムを構築し、ユーザーが「育休はいつまでに申請すればいいの?」という質問に対して回答するというユースケースになります(図が細かいので、クリックして拡大して見て下さい)。

 

1. ドキュメント取得

まずは、育児休業規約のテキストファイルを読み込みます。ここではAzure Functionsを使っていますが、別になんでもいいです。

2. 分割して登録

「1. ドキュメント取得」にて、取得した育児休業を分割してAzure Blob Storageに保存します。分割して格納する理由はGPT-3.5系のモデルでの制限である「一度に処理できるトークンは4000トークンまで」を回避するものです。つまり、4000トークンに収まる範囲で分割して、それぞれを別々のテキストファイルで格納します。なぜ、こんな回避策が必要なのかは、後プロセスで説明します。

3. 定期的にクロールしてインデックス化

Azure Cognitive Searchのインデクサーの機能によって、Azure Blob Storageに格納されたテキストを定期的にクロールして、インデックス化します。ここまでが定期的にバックエンドで行われる処理です。

4. プロンプト入力

ユーザーは育児休業はいつまでに申請すればいいのかを知りたいとします。そこで、ユーザーはブラウザなどのクライアントから、「育休はいつまでに申請すればいい?」と入力します。

5. 検索クエリ生成依頼

App Serviceなどで動作しているWebアプリケーションは、Azure OpenAI Serviceに対して、Azure Cognitive Searchに投げるための検索クエリの生成を依頼します。先程ユーザーが入力した「育休はいつまでに申請すればいい?」をもとに検索クエリの生成を依頼します。

6. 検索クエリ返却

先程「5. 検索クエリ生成依頼」で取得した検索クエリ「育休 申請 いつまで」をWebアプリケーションに返します。

7. 検索クエリでインデックス検索

先程取得した検索クエリ「育休 申請 いつまで」をもとに、Azure Cognitive Searchに検索をかけます。

8. ドキュメント取得

先程の検索によって取得したドキュメントをWebアプリケーションに返却します。ここでは、検索クエリ「育休 申請 いつまで」というクエリの検索に対して、インデックス2に対応したドキュメント「2. 申請方法 休業開始予定日の3ヶ月前までにワークフローで申請して下さい。 」を取得して返却するとします。

9. ドキュメントをもとに回答生成依頼

「8. ドキュメント取得」で取得したドキュメントをもとにAzure OpenAI Serviceに回答依頼を生成します。ここがRAGの真骨頂です。以下のようなプロンプトをAzure OpenAI Serviceに投げます。

「育休はいつまでに申請すればいい?」という質問に、以下の内容をもとに回答して。

休業開始予定日の3ヶ月前までにワークフローで申請して下さい。

「4. プロンプト入力」でユーザーが問いかけた質問に対して、Azure Cognitive Searchに登録済みの分割された育児休業規約をもとに回答を生成します。

これで、Azure OpenAI Serviceは、独自データに基づく回答を生成していることがわかりますでしょうか?

そして、ここで「2. 分割して登録」で分割したメリットが活きてきます。「一度に処理できるトークン」で説明しましたように、GPT-3系では4000トークンまでしか処理できません。先に説明したプロンプトを4000トークン以内に収めるためには、回答のもととなるドキュメントを少なくとも4000トークン以下に収める必要があり、ということは、Azure Cognitive Searchに登録するドキュメントを4000トークン以下に収める必要があり、ということは、Azure Blob Storageに登録する育児休業規約は4000トークン以下に分割して登録する必要がある、、、というわけです。

10. Webアプリケーションに回答返却

9のプロンプトをもとにAzure OpenAI Serviceが生成した「休業開始予定の3ヶ月前です」という回答をWebアプリケーションに返却します。

11. ユーザーに回答返却

Webアプリケーションがユーザーに回答を返却します。

 

といった感じで、こんな具合で独自データに基づく回答生成を行います。

どうでしょうか?はっきりって、めちゃめちゃ手間かかると思いませんか?まずはAzure Cognitive SearchやAzure Blob Storageなどのリソースも作成しなければなりませんし、Azure Blob Storageには就業規約を分割して格納しなければいけませんし、ユーザーのプロンプトをもとに検索クエリを生成しなければなりませんし、なんだかんだでAzure OpenAI Serviceに2度もリクエスト投げなければいけませんし。。。

そこで、Add your dataという機能の出番なのです。

Add your dataは、先に説明したRAGのプロセスの以下の部分をAzureが肩代わりして実行してくれます。

 

ただ、処理を肩代わりしてくれるだけで、ドキュメントをインデックス化するAzure Cognitive Searchや、ドキュメントの格納先であるAzure Blob Storageは自分で用意しなければなりません。

また、検索クエリの生成や、回答生成する際のプロンプトもAzureが裏側で勝手にやってくれますが、逆にいうと、プロンプトをカスタマイズできません。思うような回答が得られない場合にプロンプトチューニングしようと思ってもできないという自由度の低さが難点ではあります。ただ、これはマネージドなサービス全般に言えるデメリットであるので、コストや工数とのトレードオフを十分に考える必要があると言えます。

Add your dataによる実現

ではAdd your dataを用いて、独自ナレッジに基づいて回答を生成するAIを作ってみましょう。

先程も説明したようにAdd your dataは検索クエリなどの生成をAzureが肩代わりしてくれる仕組みであり、結局必要になるAzureリソースはAdd your dataを使っても変わりません。

データソース

そして、もう一つ重要な概念として、Add your dataは設定のときに選択する「データソース」の項目によって、Add your dataの管理する範囲が異なってきます。データソースは「Upload files」「Azure Blob Storage」「Azure Cognitive Search」の3つから選択できます。

■ Upload files

データソースとして「Upload files」を選択すると、Azure Cognitive Search、Azure Blob Storageの管理をAdd your dataがやってくれます。

 

上図のように、ユーザーはAzure OpenAI Studioからドキュメントをアップロードするだけで、後はAdd your data側ですべてやってくれます。Azure Cognitive Search、Azure Blob Storageのリソースを意識する必要はありません。

■ Azure Blob Storage

データソースとして「Azure Blob Storage」を選択すると、Azure Cognitive Searchの管理をAdd your dataがやってくれます。Azure Blob Storageの管理は、管理者側で行う必要があります。

上図のように、管理者はAzure Blob Storage上に作成したコンテナに、ドキュメントを自らアップロードする必要があります。

■ Azure Cognitive Search

データソースとして「Azure Cognitive Search」を選択すると、Azure Cognitive Search及びAzure Cognitive Searchのサポートするデータソースの管理を管理者で行う必要があります。これは最も手間がかかりますが、一番自由度の高い選択肢と言えます。

上図のように、Azure Cognitive Searchのサポートするデータソース(Azure Blob StorageやCosmosDBなど)にドキュメントをアップロードして、Azure Cognitive Searchのインデクサーの設定で、データソースからデータを取得してインデクシングする設定を管理者側で行います。

Add your dataは、指定されたAzure Cognitive Searchのインデックスに基づいて、独自データに基づいた回答を行います。

先の2つの手法「Upload files」「Azure Blob Storage」では、データソースがAzure Blob Storage固定だったのに比べて「Azure Cognitive Search」では多岐にわたるデータソースを選ぶことができます。

そして、実は「Upload files」「Azure Blob Storage」では、ドキュメントを格納するインデックスのフィールドのアナライザーが英語になってしまいます。つまり、英語であることを前提にAzure Cognitive Searchが検索するので、日本語のドキュメントを格納しても、制度の高い検索が行われないことがあります。

なので、日本語のドキュメントに対して制度の高い検索を行いたい場合、必然的にデータソースは「Azure Cognitive Search」選択する必要があります。「Azure Cognitive Search」であれば、インデックスの作成も管理者が行うことになるので、このあたりを自由に制御することが可能だからです。

Add your dataを使ってみよう

ということで、まずはAzure Cognitive Searchのリソースを作成します。

Azureポータル上部の検索テキストボックスに、「azure cognitive search」と入力して「Azure Cognitive Search」をクリックします。

 

サブスクリプションとリソースグループは環境にあったものを適宜入力します。サービス名はAzure Cognitive SearchのリソースをAzure全体で一意に識別できる名称を入力して下さい。場所はAzure Cognitive Searchと同じリージョンであることが望ましいです。価格レベルは、Free以外を選択して下さい(FreeだとAdd your dataの機能を有効化できません)。最後に「次:スケール」をクリックします。

 

レプリカとパーティションを入力して下さい。最後に「確認および作成」をクリックします。

 

以下の内容に問題がないことを確認して、「作成」をクリックします。「エンドポイントの接続」が公開(インターネットからフルアクセス)になっていますが、本番運用するときはIPアドレスで制限したり、サービスエンドポイント、プライベートエンドポイントなどを活用して、要件に応じでアクセスを制限して下さい(今回はAdd your dataの機能検証がメインですので、このあたりは多くは語りません)。

 

次にAzure Blob Storageのリソースを作成します。Azureポータル上部の検索テキストボックスに、「blob」と入力すると表示される「ストレージアカウント」をクリックします。

 

「+作成」をクリックします。

 

サブスクリプションとリソースグループは環境にあったものを適宜入力します。ストレージアカウント名はストレージアカウントのリソースをAzure全体で一意に識別できる名称を入力して下さい。場所はAzure Cognitive Searchと同じリージョンであることが望ましいです。他の設定値については、なんでもOKです。最後に「レビュー」をクリックします。

 

以下の内容に問題がないことを確認して、「作成」をクリックします。

 

次に、コンテナーを作成します。このコンテナーには、就業規約などの独自データが定義されているドキュメントを格納します。ストレージアカウントのリソースにアクセスして、「コンテナー」→「+コンテナー」の順にクリックします。「名前」の部分にコンテナーの名称を入力し、「作成」をクリックします。

 

そしてこのコンテナーに以下のドキュメントを格納して下さい。

Contoso株式会社 育児休業規約

目的
この規約は、TechCorpの従業員が子育てと仕事の両立を図るための支援を受けることができるようにするためのものです。

対象者
全ての正社員を対象とします。

申請資格
従業員の子どもが1歳になる日まで、または子どもが入学する日までを対象とします。

休業期間
最長で1年間。必要に応じて延長することができる場合があります。

給与について
育児休業中の給与は基本給の50%を支給します。ただし、従業員の希望に応じて無給とすることも可能です。

復帰について
休業終了後、元の職場または同等の職場に復帰することが保証されます。

申請方法
育児休業を希望する場合は、休業開始予定日の3ヶ月前までに人事部に書面で申請してください。

その他
育児休業中も社会保険や福利厚生のサービスは継続されます。休業期間中の詳細な待遇や条件については、人事部に相談してください。

 

これで必要なリソースはすべて作成されました。Add your dataの設定をするために、Azure OpenAI Studioにアクセスして、「チャット」→「データの追加(プレビュー)」→「+データソースの追加」の順にクリックします。

 

以下のような画面が表示されます。

以下に記載した内容に基づいて入力して下さい。入力したら「次へ」をクリックします。

データソースを選択する

データソース」のところで説明したデータソースの選択になります。今回は「Azure Blob Storage」選択します。

サブスクリプション
事前に作成したAzure Cognitive SearchやAzure Blob Storageと同じものを指定します。
Azure Blob Storageリソースの選択
先程作成したAzure Blob Storageを選択します。
ストレージコンテナを選択してください

先程作成し、ドキュメントを格納したコンテナーを選択して下さい。

インデックス名
Add your dataがAzure Cognitive Searchに自動で作成してくれるインデックスの名前を指定します。任意のわかりやすい名称でOKです。
インデクサーのスケジュール
「Once」(一度きり)、「Hourly」(¹時間に¹回)、「Daily」(1日に1回)を選択できます。どれでもOKです。
ベクトル検索をこの検索リソースに追加します。
チェックすると、より精度のたかいベクトル検索が有効になりますが、今回は使いません(設定がややこしくなるので)。
Azure Cognitive Searchアカウントに接続すると、アカウントが使用されるようになることを同意します。
Azure Cognitive Searchは結構お値段がはるので、本当に使っていいですか的な確認かと思われます、、、多分。チェックしないと次に進めないので、チェックします。

 

「キーワード」を選択して「次へ」をクリックします。

 

設定に問題ないことを確認して、「保存して閉じる」をクリックします。

 

以下のような画面が表示されます。これはAdd your dataのインデックス作成処理が進行中であることを表しています。

 

以下のような画面になれば完了です。

 

プレイグラウンドで試してみますと、たしかにAzure Blob Storageにアップロードした独自データに基づいて回答しているのがわかりますヮ(゚д゚)ォ!

統合開発環境Promplt flow

Azure OpenAI Serviceには、フローを開発するための便利機能がたくさん詰まっている「Prompt flow」という統合開発環境があります。

フローの開発・テスト・評価・デプロイなど、フローのリリースに至るまでの様々な支援機能がある開発環境です。

ここであるユースケースを想定し、「Prompt flowを使わない場合」と「Prompt flowを使う場合」で、その便利さをわかりみ深く説明したいと思います。

で、そのユースケースとは、以下になります。

特定のURLの記事のカテゴリを分類する。 

例えばアプリの紹介記事だったら「App」、アカデミックな内容だったら「Academic」などに分類します。

そして、この要件を満たすフローを以下のように設計したとします。

 

それぞれの処理を以下に説明します。

■ 指定したURLからコンテンツを取得する

一番最初に与えられるInputであるURLにアクセスし、その中身のテキストを取得します。画像データなどのバイナリは取得しません。この取得したテキストを以降では「コンテンツ」と呼ぶことにします。

■ コンテンツを要約する

Azure OpenAI Serviceに、先程取得したコンテンツの要約を依頼します。

■ 要約したコンテンツをもとにカテゴリを決定する

先程要約したコンテンツをもとに、Azure OpenAI Serviceにカテゴリの決定を依頼します。

■ データを整形する

APIなどで取得しやすいようにデータを整形します。具体的にはJSONに変換します。

Prompt flowを使わない場合

では先程のユースケースに基づいたフローを、Prompt flowを使わないで開発する場合を考えてみましょう。簡単に図にまとめてみました。左がフローを開発するために必要なプロセス、左がそのプロセスを実現するための手段になります(字が細かいので拡大して見て下さい)。

それぞれのプロセスと実現手段の詳細を以下に記載します。

■ 要件をヒアリングしてフローを設計する

これは、Prompt flowを使う前のプロセスですので、Prompt flowを使わない場合でも使った場合でも変わりはありません。そしてフローは先程、説明したものを設計のアウトプットとして、以降のプロセスで利用します。

■ フローを開発する

設計したフローをもとに、Visual Studio Codeなどのエディタを用いて開発を行います。フローの中ではAzure OpenAI Serviceにアクセスする部分があるので、SDKや汎用的なライブラリであるLangChainなどを使ってコードを書く必要があります。Azure OpenAI Serviceに接続するための資格情報も厳密に管理する必要があります。

■ 大量のデータでテストを行う

設計・開発したフローやプロンプトの妥当性を図るために、大量のデータでテストを実施します。記事のURLと、その記事の想定カテゴリを定義したテストデータを用意して、開発したフローに読み込ませ、実行結果と期待値の比較を行います。

例えば以下のようなCSVデータを用意します。

https://tech-lab.sios.jp/archives/37250,Academic
https://contoso.com/healthcare,App

1列目は記事のURL、2列目はその記事がカテゴライズされるであろうカテゴリになります。

1行目のhttps://tech-lab.sios.jp/archives/37250はAcademicのカテゴリ、2行目のhttps://contoso.com/healthcareはAppのカテゴリにカテゴライズされることを想定しています。

このテストデータのCSVを読み込み、フローによる出力結果と期待値を比較するテスト用プログラムを書く必要があります。

まぁ、通常の開発プロセスではありますが、だるい作業ではあります。

■ 本番環境にデプロイしてエンドユーザーに利用してもらう

開発とテストが終わったらいよいよリリースです。Azureであれば、Virtual MachineやApp Serviceなどのコンピューティングリソースを用意して、開発したフローが動くアプリケーションをビルドしてデプロイして、、、という作業が必要になります。

うーん、めんどくさい。

そしてユーザーのフィードバックがあった場合には、「フローを開発する」のプロセスに戻り、開発・テスト・リリースというプロセスを繰り返します。

Prompt flowを使う場合

では、Prompt flowを使った場合の開発プロセスを説明します。先程と同様に図にまとめてみました(字が細かいので拡大して見て下さい)。

■ 要件をヒアリングしてフローを設計する

これは先程と同様、Prompt flowを使う前のプロセスですので、Prompt flowを使わない場合でも使った場合でも変わりはありません。

■ フローを開発する

Webで提供されている専用の開発環境「Machine Learning Studio」内にある「Prompt flow」を用いて行います。Visual Studio Codeなどの統合開発環境の用意及びそれを動かすためのコンピューティングリソースは不要です。また、Azure OpenAI ServiceやOpenAIに接続するためのライブラリや、それを用いた実装も不要です。あらかじめ、OpenAIやAzure OpenAI Serviceの接続情報を定義しておけば、テンプレートに基づいてプロンプトを書くだけで、自動でOpenAIやAzure OpenAI Serviceに接続してくれます。

■ 大量のデータでテストを行う

Batch runという機能が用意されており、テストデータと期待値が定義された所定のCSVを読み込ませるだけで、テストの実行と結果の出力まで自動でやってくれます。

■ 本番環境にデプロイしてエンドユーザーに利用してもらう

フローを動作させるためのVirtual MachineやApp Serviceなどのコンピューティングリソースをわざわざ用意する必要はありません。「Deploy」というボタンをクリックし、以降のウィザードに従い、インスタンスサイズなどを指定するだけで、フローを動かすための環境が作成され、エンドポイントの発行も自動で行われます。

Prompt flowを使って開発してみよう

Prompt flowの凄さがおわかり頂けかと思います。では、早速Prompt flowを使って、先の要件に基づくフローを開発してみたいと思います。

具体的な手順に入る前に、Prompt flowの画面構成を説明いたします。画面左部はコードや入出力の定義、画面右部はフローの流れを表したものになっています。①はフローの一番最初に入力する値を定義し、②は最終的に出力される値を定義します。③はフローの中のそれぞれの処理のコードや設定情報になります。④はフローを構成する処理のことで「ノード」と呼びます。そしてノードが集まって全体の流れを形成しているもののことを「フロー」と呼びます。

 

ま、何はともあれ手を動かして作ってみないとわからないので、早速やってみたいと思います。

ワークスペースの作成

まずはワークスペースを作成します。ワークスペースは開発環境を管理する最上位の単位になります。プロジェクトや機能ごとにワークスペースを作成します。

ワークスペースを作成するためには、以下のURLにアクセスして、Machine Learning Studioを開きます。

https://ml.azure.com/

 

画面左部メニューにある「ワークスペース」をクリックします。画面左部に「+新規」が表示されますので、それをクリックします。

 

「ワークスペース名」はワークスペースを一意に識別できる任意の名前を入力します。「サブスクリプション」「リソースグループ」「リージョン」はAzure OpenAI Serviceがあるリソースと同等のものにします。最後に「作成」をクリックします。

 

以下のように新しいワークスペースが一覧に表示されるので、それをクリックします。

Azure OpenAI Serviceへの接続情報の作成

プロンプトフローからAzure OpenAI Serviceに接続するために、接続情報の作成を行います。

「プロンプトフロー」→「接続」→「+作成」→「Azure OpenAI」の順にクリックします。

 

以下のような画面が表示されます。

以下の内容に従い、必要事項を入力して、「保存」をクリックします。

名前

接続を一意に識別できる任意の名前を入力します。

プロバイダー
接続先の種別になります。OpenAIやAzure OpenAI Service、Azure Cognitive Searchなどがありますが、ここではAzure OpenAI Serviceに接続したいので、「Azure OpenAI」を選択します。
サブスクリプションID
接続先のAzure OpenAI Serviceがあるサブスクリプションを選択します。
Azure OpenAIアカウント名

接続したいAzure OpenAI Serviceのリソースを選択します。

API key
接続したいAzure OpenAI ServiceのAPIキーを取得します。取得方法については、「デプロイを指定してAPIを発行」を参考にして下さい。
API base
接続したいAzure OpenAI ServiceのAPI baseを取得します。「デプロイを指定してAPIを発行」に記載の「②Language APIs」の値です。
API type
「azure」を指定します。
API version
APIのバージョンを指定します。バージョンについてはこちらのURLに記載があります。

 

新規フローの作成

まずはフローを作成します。ワークスペースの左部メニューに表示されている「プロンプトフロー」をクリックします。次に、画面左部に表示される「+作成」をクリックします。

 

様々なフローのテンプレートが表示されます。今回はゼロから作成するので、「標準フロー」をクリックします。

 

フローの名前を入力して、「作成」をクリックします。

 

すでにデフォルトでいくつかの入力、出力、ノードが作成されているので、これらを削除します。下図赤枠のゴミ箱マークをクリックしてすべて削除して、最後に「保存」をクリックします。

ランタイムの作成

このフローを実行するためのランタイムを作成します。ランタイムとは、フローを実行するためのコンピューティングリソースのことです。もうちょっと平たく言いますと、フローを実行するためにはもちろんCPUやメモリが必要になります。そのCPUやメモリのことをコンピューティングリソースと呼び、「ランタイムを作成する」ということは、フローの実行に必要なCPUやメモリを割り当てるということと同じです。

プロンプトフロー画面上部にて、「+ランタイムの追加」→「コンピューティングインスタンスランタイム」の順にクリックします。

 

「ランタイム名」にはランタイムを一意に識別する任意の名称を入力します。次に「Azure ML コンピューティングインスタンスを作成する」をクリックします。

 

「コンピューティング名」には、コンピューティングインスタンスを一意に識別する任意の名称を入力して下さい。「仮想マシンの種類」はCPUをチェックして下さい。「仮想マシンのサイズ」はフローに応じて必要なスペックのものを選択して下さい。最後に「確認と作成」をクリックします。他にも色々と細かい設定項目があるのですが、今回は割愛します。

 

以下の内容に問題がないことを確認して「作成」をクリックします。

 

「Azure ML コンピューティングインスタンスを選択する」をクリックして、先程作成したコンピューティングインスタンスが「実行中」であることを確認して、そのインスタンスを選択します。「カスタムアプリケーション」は「新規」にチェックして下さい。「環境」は「既定の環境を使用する」をチェックします。独自のライブラリなどを使いたい場合は、独自にカスタマイズしたDockerイメージをもとに新しいランタイムを作成しますが、今回は標準のもので事足りるので、それはしません。最後に「作成」をクリックします。

 

ランタイムの作成にはちょっと時間がかかります。「ランタイム」の部分が、以下のような表示になっていればOKです。

 

入力の作成

先程設計したフローの最初の入力値である「URL」の入力を作成します。

 

「+入力の追加」をクリックします。

 

「名前」に「url」と入力します。これは変数名のようなものです。「種類」は「string」です。これは変数の型に相当します。「値」には任意のURLを入力します。ここで入力したURLがカテゴライズの対象の記事となります。

指定したURLからコンテンツを取得するノードの作成

指定したURLからコンテンツを取得するノードを作成します。

 

ノードには様々な種類があります。Pythonのコードを実行するノード、OpenAIやAzure OpenAI ServiceのようなLLMにアクセスするノードなど様々です。今回作成するノードでは、特定のURLからテキストを抜き取る処理をPythonで実行しますので、Pythonのノードを作成します。ということで、下図の①をクリックします。そして②の部分にノード名「fetch_text_content_from_url」を入力して、「追加」をクリックします。

 

下図のようなノードが出来上がります。そして赤枠の部分に、指定したURLからコンテンツを取得するコードを入力する必要があります。

 

以下が、指定したURLからコンテンツを取得するコードになります。詳細な処理の内容はコメントに記載しております。

from promptflow import tool
import requests
import bs4

# このアノテーションが付与されてる関数がPrompt flowから呼び出されます。
# このコード内で一つしか定義できません。
@tool
def fetch_text_content_from_url(url: str):
    # Send a request to the URL
    try:
        headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
                                 "Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35"}
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            # BeautifulSoupというライブラリを用いて、HTMLを解析し、タグを除いたテキスト部分だけを取得します。
            soup = bs4.BeautifulSoup(response.text, 'html.parser')
            soup.prettify()
            # 取得したテキストを返します。次のノードで利用します。
            return soup.get_text()[:2000]
        else:
            msg = f"Get url failed with status code {response.status_code}.\nURL: {url}\nResponse: " \
                  f"{response.text[:100]}"
            print(msg)
            return "No available content"
    except Exception as e:
        print("Get url failed with error: {}".format(e))
        return "No available content"

 

先のコードを入力します。そして、「入力値の検証と解析」をクリックします。

 

すると「入力」の「名前」の部分が「url」になります。これは、@toolアノテーションが関数の引数の変数名に置き換えられた形になります。そして、「値」の部分のセレクトボックスで「${input.url}」という値が選択可能となっております。これは先の「入力の作成」で定義した入力値になります。つまり入力のところで定義した変数は${input.入力の名前}で定義されるということです。ということで「${input.url}」を選択します。

 

すると、画面右部のグラフで、「inputs」(入力)と「fetch_text_content_from_url」が矢印で繋がりました。これは、「inputs」(入力)で定義した入力値URLが、fetch_text_content_from_urlの関数fetch_text_content_from_urlの引数に渡されていることを意味します。こんな感じで、前のノードの出力結果を次のノードに渡していきます。基本的には、これ以降でやる作業についても、この流れでのノードを繋げていきます。まさにフローの構築ですね。

コンテンツを要約するノードの作成

コンテンツを要約する処理を担当するノードを作成します。

「+LLM」をクリックします。これはOpenAIやAzure OpenAI ServiceのようなLLMを利用するためのノードになります。

 

「summarize_text_content」と入力して、「追加」をクリックします。

 

「接続」のセレクトボックスをクリックして、先程作成したAzure OpenAI Serviceへの接続を選択します。これでこのノードからAzure OpenAI Serviceに接続できるようになりました。

 

新たに「API」というセレクトボックスが現れます。これは使うAPIの種類を表すものであり、「chat」「completion」のどちらかを選択するのですが、completionは「チャット」で説明したように非推奨なので、「chat」を選択します。「deployment_name」は作成済みのデプロイを選択します。

 

LLMのノードではプロンプトを与える必要があります(当然ですね)。そのプロンプトは以下になります。Prompt flowではLLMに与えるプロンプトはjinja2というテンプレートをベースに作成します。「system」「user」はこちらで説明した「system」「user」と同じものになります。systemには「以下の文章を100ワードの1段落でまとめて下さい。文章の中にあること以外は回答しないでね」という感じでAIのキャラ付をしています。userには{{text}}で表される文章の要約を出すようにLLMに依頼しています。

system:
Please summarize the following text in one paragraph. 100 words.
Do not add any information that is not in the text.

user:
Text: {{text}}
Summary: 

 

上記のコードを入力して、先程と同様に「入力の検証と解析」をクリックします。すると、入力の「名前」の部分が「text」に代わりました。これはプロンプト内の{{text}}から取得した値になります。そして入力の「値」をクリックすると、いつくか選択肢が出てきました。1つ目の${input.url}は「入力の作成」で定義したURLになります。2つ目の「${fetch_text_content_from_url.output}」は、「指定したURLからコンテンツを取得するノードの作成」で取得したURLのコンテンツになります。つまり、${fetch_text_content_from_url.output}を選択しますと、プロンプト内の{{text}}は、${fetch_text_content_from_url.output}の値で置換されます。つまり、最初に指定したURLのコンテンツの要約をLLMに依頼するプロンプトの完成となるわけです。

 

いい感じでフローが出来上がってきました。

コンテンツを要約するノードの作成

要約したコンテンツをもとにカテゴリを決定するノードを作成します。

 

カテゴリの決定もAzure OpenAI Serviceを使いますので、「+LLM」をクリックします。

 

classify_with_llm」を入力して、「追加」をクリックします。

 

「接続」「API」「deployment_name」は先程のLLMノードと同じにします。

 

このノードに与えるプロンプトは以下のとおりです。

system:
Your task is to classify a given url into one of the following categories:
Movie, App, Academic, Channel, Profile, PDF or None based on the text content information.
Here are one example:
URL: https://tech-lab.sios.jp/archives/37250
Text content: hogehoge
OUTPUT:
{"category": "App"}

user:
The selection range of the value of "category" must be within "Movie", "App", "Academic", "Channel", "Profile", "PDF" and "None".

For a given URL and text content, classify the url to complete the category:
URL: {{url}}
Text content: {{text_content}}.
OUTPUT:

systemでは、与えられたURLの記事を「Movie」「App」「Academic」「Profile」「PDF」「None」に分類するAIであることをキャラ付けしています。

userでは、URLとコンテンツをもとに「Movie」「App」「Academic」「Profile」「PDF」「None」のいずれかのカテゴリに分類してねとLLMに依頼をしています。

 

先程のプロンプトをノードの「プロンプト」に貼り付けて、「入力の検証と解析」をクリックします。これも先程と同様プロンプト内の{{url}}{{text_content}}に適切な入力値を割り当てるために、以下のように設定します。

 

もうそろそろ完成です。

データを整形するノードの作成

データを整形するノードの作成をします。

ノードの出力結果はJSONのoutputというフィールドに格納されます。例えば、先のclassify_with_llmノードの出力結果は以下になります。

[
	{
		"system_metrics": {
			"completion_tokens": 6,
			"duration": 0.217636,
			"prompt_tokens": 259,
			"total_tokens": 265
		},
		"output": "{\"category\": \"Movie\"}"
	}
]

 ご覧になるとわかりかと思いますが、outputフィールドの中に結果は格納されているのですが、文字列として扱われています。JSONとして扱えないと非常に都合が悪いです。

よって、このノードではこの文字列として扱われている値をJSONに変換します。

Pythonのノードを作成するので、「Python」をクリックします。

 

「convert_to_dict」と入力して、「追加」をクリックします。

 

以下のPythonコードを「コード」の部分に入力します。

from promptflow import tool
import json

@tool
def convert_to_dict(input_str: str):
    try:
        return json.loads(input_str)
    except Exception as e:
        print("input is not valid, error: {}".format(e))
        return {
            "category": "None",
            "evidence": "None"
        }

 

先程と同様にコードを入力したら、「入力の検証と解析」をクリックして、ノード「classify_with_llm」の出力結果が、このノードの入力になるように設定します。

 

もうちょいです。

出力の作成

最終的に出力される値を作成します。ここで定義した値は、APIでの出力や、後に説明するテスト(batch run)でも使われます。

 

convert_to_dictノードにより、categoryのJSONは文字列ではなく、JSONとして出力されるようになりました。よって、最終的な出力を以下のように定義します。

 

動かしてみよう

フローが完成したので動かしてみたいと思います。では、以前私が書いたブログを入力のURLに定義してみたいと思います。

世界一わかりみの深いAzure Application Gateway

 

「入力」の「url」の値に、先程のURLを入力します。

 

「実行」をクリックします。

 

実行が成功すると、それぞれのノードでの出力結果が表示されます。

 

フロー全体での結果を見てみます。「出力の表示」をクリックします。

 

以下のように「入力」で定義した入力値urlと、「出力」で定義した出力値categoryが表示されています。どうやら先程の私のブログは「App」に分類されたようです。

 

ちなみに映画.com(https://eiga.com/)は、Movieに分類されました。なんとなく正しく動いているようです。

 

テストの実行

フローが完成したところで、大量のデータを使ってテストをしてみます。

プロンプトが妥当かどうかは、大量のテストデータと期待値を使って、テストをするのが最も良い方法です。もし期待された結果と異なるのであれば、プロンプトの修正が必要になります。例えば、こちらで説明した牛タンゲームの例のように、Few-shot Learningさせて上げる必要があるかもしれません。

そんな大量データのテストも、Prompt flowではコードを書くことなく実行ができます。

今回、以下のようなテストデータを使ってテストをしてみます。「入力値(url)」をフローに処理させた結果が、「期待される結果(category)」と一致していればテストは成功です。今回は、テストに失敗した例も見てみたいので、3番目は意図的に失敗すテストデータになっています。

入力値(url)期待される結果(category)
https://tech-lab.sios.jp/archives/30628App
https://eiga.comMovie
https://tech-lab.sios.jp/archives/31704Movie

 

Prompt flowでは、CSVでテストデータを作ることができます。必ず1行目にはヘッダが必要になります。今回は以下のようなデータを作成します。

url,category
https://tech-lab.sios.jp/archives/30628,App
https://eiga.com,Movie
https://tech-lab.sios.jp/archives/31704,Movie

 

「評価」をクリックします。

 

「実行表示名」を入力します。このテスト実行を一意に識別する任意の名称でOKです。${variant_id} は、ノード バリアント ID、${timestamp} は送信タイムスタンプに置き換えられます。「次へ」をクリックします。

 

「ランタイム」はテストを実行するランタイムです。フローを実行したのと同じものを指定します。テストに大量のコンピューティングリソースを必要とする場合は、専用のランタイムを作成してもよいかもしれません。次に「+新しいデータの追加」ををクリックしてテストデータを登録します。

 

「Name」にはテストデータを一意に識別する任意の名称を入力します。「ファイルを選択」には、先程作成したテストデータを選択します。最後に「追加」をクリックします。

 

「入力マッピング」では、テストデータの入力値として使われる値が、CSVどのフィールドにマッピングされるかを定義します。CSVの1列目、つまり一行目のヘッダー列がurlに相当する値が、テストデータの入力値となります。そして、値の書式は${data.[CSV1行目の列名]}になります。よって、ここでは「${data.url}」を選択します。最後に「次へ」をクリックします。

 

評価の方法を選択します。様々な評価の方法がありますが、今回のように、入力値と出力値が単純にマッチしてるかどうかを評価するためには「Classify Accuracy Evalution」を用います。「Classify Accuracy Evalution」にチェックを入れて、「次へ」をクリックします。

 

「実際の出力結果」と「期待される出力結果」のマッピングを行います。「groundtruth」には期待される出力結果を入力します。それはCSVのcategroy列に定義されている値なので、「データ入力」「category」を選択します。

そして、「実際の出力結果」は、「出力の作成」で作成したcategoryという名前の値になります。なので、「フロー出力」の「category」を選択します。最後に「次へ」をクリックします。

 

内容に問題ないことを確認して「送信」をクリックします。テストが始まります。

 

「実行リストの表示」をクリックするとテスト結果を見ることができます。

 

テスト実行結果の一覧が表示されます。「状態」が「完了」となっていることを確認して、表示名の部分をクリックします。

 

「詳細」をクリックします。

 

「関連する結果の追加」の隣のセレクトボックスをクリックして、以下を選択します。

 

「grade」の列にテストの実行結果が表示されています。想定通り3つ目のテスト結果は、期待される結果が「Movie」ナノに対して、出力された結果は「App」なので、gradeが「incorrect」(テスト失敗)になっています。

 

これでテストの実行ができました。ノーコードで簡単に大量データのテストが実行できましたね。すごい!!

デプロイ

最後はデプロイになります。デプロイして初めて、エンドポイントが発行され、APIとして実行できるようになります。

フローにアクセスして、「デプロイ」をクリックします。

 

以下のような画面が表示されます。

以下の内容に基づいて必要事項を入力して、「次へ」をクリックします。。

エンドポイント

「新規」「既存」のどちらかを選択します。今回は新しく作成するので「新規」になります。既存のエンドポイントを修正したい場合は「既存」を選択します。

エンドポイント名
エンドポイントを一意に識別する名称を入力します。
認証の種類
「トークンベースの認証」の場合は、「Microsoft Entra IDによる認証」で説明したように、Microsoft Entra IDから払い出されるトークンによって認証することができます。「キーベースの認証」は「デプロイを指定してAPIを発行」で説明したような無期限のAPIキーによる認証です。今回は「キーベースによる認証」を選択します。

IDの種類

エンドポイントを発行すると、そのコンピューティングリソースを識別するマネージドIDが発行されます。そして、このマネージドIDがAzure Machine Learningワークスペースにアクセスできる権限を付与しなければなりません。それは後の手順で説明します。ここでは「システム割り当て」「ユーザー割り当て」を選択します。それぞれの内容についてはこちらのブログを参照いただくとして、今回は手順の簡略化のために「システム割り当て」を選択します。

エンドポイントタグ
エンドポイントを識別しやすくするためのタグです。今回は未入力とします。

 

デプロイされるコンピューティングリソースの情報を入力します。「デプロイ名」はデプロイを一意に識別する任意の名称を入力します。その他は以下のようにデフォルト値で問題ありません。最後に「次へ」をクリックします。

 

APIにアクセスしたときに出力される情報を定義します。以下に表示されているのは、フロー作成のときに「出力の作成」で定義した値になります。APIの応答に含める必要があるので、「エンドポイント応答に含まれる」にチェックをして、「次へ」をクリックします。

 

LLM(今回はAzure OpenAI Service)に接続するノードで使われる接続情報を設定します。ノード作成時と同じ接続、デプロイであることを確認して、「次へ」をクリックします。

 

APIとして動作させるためのデプロイに使われるコンピューティングリソースとインスタンス数を選択して、「次へ」をクリックします。

 

内容に問題ないことを確認して、「デプロイ」をクリックします。

 

次にエンドポイント用にデプロイされたコンピューティングリソース(Virtual Machine)から、Azure Machine Learningワークスペースのリソースにアクセスできるよう、マネージドIDに対して権限を付与します。今回作成したAzure Machine Learningワークスペースのリソースにアクセスして、「アクセス制御(IAM)」→「+追加」→「ロールの割り当ての追加」の順にクリックします。

 

「AzureMLデータ科学者」を選択して、「次へ」をクリックします。

 

「マネージドID」→「+メンバーの選択」の順にクリックします。「サブスクリプション」は先程デプロイ用のコンピューティングリソース(Virtual Machine) を作成したサブスクリプションを、「マネージドID」は「すべてのシステム割り当てマネージドID」を選択します。「選択」の部分では、先程のデプロイしたコンピューティングリソース(Virtual Machine) の名前を入力して表示されたマネージドIDを選択して、「選択」をクリックします。

 

「レビューと割り当て」をクリックします。

 

「レビューと割り当て」をクリックします。

 

これで完了です。では動作確認のためにエンドポイントにAPIを発行してみましょう。

Machine Learning Studio(https://ml.azure.com/)にアクセスして、画面左部メニューの「エンドポイント」をクリックします。すると、画面右部に先程作成したエンドポイントが表示されますので、それをクリックします。

 

「使用」のタブをクリックします。「デプロイ」「RESTエンドポイント」「主キー」の値をメモります。

 

先程メモした情報をもとにcurlコマンドで以下のHTTPリクエストを発行します。

$ curl -X POST "[先程メモしたRESTエンドポイント]" \
     -H "Authorization: Bearer [先程メモした主キー]" \
     -H "Content-Type: application/json" \
     -H "azureml-model-deployment: [先程メモしたデプロイ]" \
     -d '{"url":"https://tech-lab.sios.jp/archives/30628"}'
{"category":"Academic"}

作成したフローの仕様・設定に基づいた応答が返ってきました。

 

Prompt flowを使うと、ローカルにVisual Studio Codeのような専用の開発環境の用意は不要でした。また、LLMにアクセスするためのライブラリ(LangChainなど)も使わなくてすみます。大量テストの実行も専用のテストツールを作らず、ノーコードで実現できますし、APIのエンドポイントを作成するときも、わざわざVirtual Machineを用意する必要もありません。全部Prompt flowが肩代わりしてくれます。

LLMのフローを作成するには、相当便利なサービスであることがおわかり頂けかと思います。

まとめ

いかがでしょうか?ちょっと長くななりましたが、Azure OpenAI Serviceの概要をわかりみ深く説明させていただきました。これからOpenAIやAzure OpenAI Serviceを始めるための取っ掛かりとなれば幸いです。ではステキなAIライフを。

Happy Azure!!

アバター画像
About 武井 宜行 271 Articles
Microsoft MVP for Azure🌟「最新の技術を楽しくわかりやすく」をモットーにブログtech-lab.sios.jp)で情報を発信🎤得意分野はAzureによるクラウドネイティブな開発(Javaなど)💻「世界一わかりみの深いクラウドネイティブ on Azure」の動画を配信中📹 https://t.co/OMaJYb3pRN
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

8人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる