AIで一問一答を自動生成するAPIを作った話

# 完成品
![スクリーンショット 2022-06-03 18.42.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/6227f908-5d69-626d-2bbd-5a3e24333225.png)


# 一問一答を自動生成したいという夢
私は一問一答形式の勉強が好きです。
テンポよく学習し、その成果を確認できるのがいいですよね。
大学生の頃に夢見ていた、一問一答の自動生成をAIで実現しました。

# 構成図
ユーザーがキーワードを入力したURLを叩くことで、一問一答を得られる仕組みを作りました。(URLは非公開です)
![qiita-qna.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/37f1676d-8838-04f3-bf04-f107df98a24c.png)
|:--|

# 前提
## OpenAIについて
OpenAI は、人工知能を研究する非営利団体です。
## OpenAI API
OpenAIが作成した汎用的に使える学習済み言語モデルのAPIを、簡単な登録をすることで使用できます。
### サインイン
https://openai.com/api/
URLを開き、右上の「SIGN UP」ボタンから、Googleアカウント連携でサクッと登録が可能です。
### デモ
メニューにあるPlaygroundからOpenAIのAPIを試すことができます。
補完モードで、「MIME」に関する一問一答を作って、と入力し実行した画像です。
緑色ハイライトがAIが生成した文章です。
![スクリーンショット 2022-06-02 11.31.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/7ef94f77-34a3-b67f-2dff-3bda6131507c.png)
|:--|

# コンセプト
「学習済みの汎用言語モデルに、一問一答の出題パターンを数個入力として与えて、そのパターンに合わせて補完させる」という方法で、一問一答AIを作ります。
一問一答といえば、「問題文、数個の選択肢、解答」の３つから成り立っていますよね。
今回は、何に関する一問一答を作るのか、を指示する文章を含めた
```
Inst : 指示文（ここにキーワードや文を入れる）
Q: 問題文
C: 選択肢
A: 解答
```
というパターンを与えます。具体的には以下のようになります。
![スクリーンショット 2022-05-30 16.19.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/7b07a7dd-372d-6b20-cde7-e822ec19b86b.png)
|:--|

基本情報試験の一問一答をパターンとして与えました。
最後に、`{ここに入力}`の欄に、任意のフレーズを入れて実行することで、あとはAIが補完してくれるという仕組みです。

## コンセプトの検証
では、実際にどうなるか見てみましょう。
※パターンを学習させるための一問一答の例は省略しています

---

**チャットボット**を入れて補完を実行

![スクリーンショット 2022-05-30 16.14.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/d46ddc4f-3c35-0ad4-4d36-0684d864f398.png)
|:--|

---


**PMBOK**を入れて補完を実行
![スクリーンショット 2022-05-30 16.16.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/b4df5e29-fed8-a4bc-7ba5-ebf8d1ac1546.png)
|:--|
---


いい感じに一問一答が作成できていますね。
ダメな例も見てみましょう。
![スクリーンショット 2022-05-30 16.28.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/4ee4f351-ebe4-14cb-ba43-515617460fe2.png)
|:--|

生成される一問一答が必ずしも正しいとは限らないので、要注意です。

ちなみに、１回の実行で\\$0.05 ~ $0.11かかりました。（[APIの費用](https://openai.com/api/pricing/)）
パターンを学習させる部分を毎度毎度入力として与えているので、その分ちょっと使用料が高くなってしまいます。

# 留意点について
今回の留意点をまとめました。
* 学習させたパターンに返答が影響されます
    * 基本情報技術者の一問一答をパターンとして与えたので、違うジャンルのキーワードを入力しても、ITや技術系に寄った出題文が生成されることがあります。
* AIが生成した文章は必ずしも正確ではありません
    * AIが生成する一問一答はしばしば間違っています。
* OpenAI APIを使用する際にはガイドラインの確認を
    * 使用ガイドラインは[こちら](https://beta.openai.com/docs/usage-guidelines/app-review)

# 実装


## AWS Lambda
HTTPリクエストを受け取って、OpenAI APIを呼び出し、その応答を返す関数をLambdaで作ります。

簡易的なAPIはAWS Lambdaを使うとすぐにできます。
以下の3つに注意して関数の作成を行います。
* 詳細設定 > 「関数URLを有効化」にチェックを入れること
* 環境変数(OPENAI_API_KEY)を設定すること
* 一般設定 > タイムアウトを30秒に延長すること

※OpenAIのAPIキーの取得方法は[こちら](https://qiita.com/ssc-yshikeda/items/acae1fa00319ba3c4d97#apiキーの取得方法)

### コード

index.jsにコードを貼り付けます。deployを押して、出来上がった関数URLの末尾に`/?word=キーワード`として、アクセスするとキーワードに関する一問一答が出力されます。

```javascript
const https = require('https');

// OpenAI APIにPOST送信するための関数
const postRequest = (body) => {
  // 環境変数にOPENAI_API_KEYを設定すること
  const apiKey = process.env.OPENAI_API_KEY;
  const options = {
    hostname: 'api.openai.com',
    path: '/v1/engines/text-davinci-002/completions',
    method: 'POST',
    port: 443,
    headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json;charset=UTF-8'
    }
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, res => {
      let rawData = '';

      res.on('data', chunk => {
        rawData += chunk;
      });

      res.on('end', () => {
        try {
          resolve(JSON.parse(rawData));
        } catch (err) {
          reject(new Error(err));
        }
      });
    });

    req.on('error', err => {
      reject(new Error(err));
    });

    req.write(JSON.stringify(body));
    req.end();
  });
}

// Lambdaへのアクセスを受け取る
exports.handler = async (event) => {
  const error = {
      statusCode: 500,
      body: "Error!"
  };
  
  if(!event["queryStringParameters"] || !event["queryStringParameters"]["word"]) return error;

  const word = event["queryStringParameters"]["word"];
  const data = {
      "prompt": `Inst: 「オプティマイザ」に関する一問一答\nQ: SQL文を実行する際に，効率が良いと考えられるアクセス経路を選択する関係データベース管理システム(RDBMS)の機能はどれか。\nC: ア)オプティマイザ, イ)ガーベジコレクション, ウ)クラスタリング, エ)マージソート\nA:  ア)\n\nInst: 「射影」に関する一問一答\nQ: 関係データベースの操作のうち，射影(projection)の説明として，適切なものはどれか。\nC: ア)ある表の照会結果と，別の表の照会結果を合わせて一つの表にする。, イ)表の中から特定の条件に合致した行を取り出す, ウ)表の中から特定の列だけを取り出す, エ)二つ以上の表の組から条件に合致した組同士を合わせて新しい表を作り出す\nA: ウ)\n\nInst: 「原子性」に関する一問一答\n\nQ: トランザクションが，データベースに対する更新処理を完全に行なうか，全く処理しなかったのように取り消すか，のどちらかの結果になることを保証する特性はどれか。\nC: ア)一貫性(consistency), イ)原子性(atomicity), ウ)耐久性(durability), エ)独立性(isolation)\nA: イ)\n\nInst: 「MIME」に関する一問一答\nQ: インターネットにおける電子メールの規約で，ヘッダフィールドの拡張を行い，テキストだけでなく，音声，画像なども扱えるようにしたものはどれか。\nC:ア)HTML, イ)MHS, ウ)MIME, エ)SMTP\nA: ウ)\n\nInst: 「${word}」に関する一問一答`,
      "temperature": 0.5,
      "max_tokens": 600,
      "top_p":1.0,
      "frequency_penalty":1,
      "presence_penalty":1
  };
  try{
    const res = await postRequest(data);
    const success = {
      statusCode: 200,
      body: res["choices"][0]["text"],
      headers: {
        'Content-type': 'text/plain;charset=UTF-8'
      }
    };
    return success;
  }catch(e){
    return error;
  }
  
};
```

# テスト

![スクリーンショット 2022-06-03 18.19.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/5203fb4d-04cf-2383-8957-d49deeb8a9dc.png)

それっぽいものができ、夢が叶いました！...が、どうも正確性に欠けているようです。
より正確にしていくためにはモデルのトレーニングがもっと必要なんですね。

以上、今回の記事が面白いと思った方は、いいね！フォローよろしくお願いします！