OpenAI(ChatGPT)のAPIを使って英検を意識した英語のリスニング問題を生成する

# はじめに
英語、大事ですよね？
リスニング、大事ですよね？
でも市販の問題集、高くないですか？
リスニングの問題、限られていませんか？
英検を受ける人、過去問形式の問題もっとほしくないですか？

作ればいいんです！最新のテクノロジーを使って！

**そう、生成AIです！**

# 前提
以下のものを含む英語のリスニング問題を生成することを目標にします。

* 問題の本文（英文、政治・科学・歴史などさまざまなジャンル）
* 本文に関連する問い二つ（英文）
* それぞれの問いに対する四択の選択肢（英文）
* 正解となる選択肢（番号）
* なぜ正解となるのかの解説（日文）
* 本文と問いの英語音声（英語音声）

また、上記に加えて本文中の難しい単語に関する詳しい日本語による解説も出力させます。つまり、これ一つだけで英語力向上に必要な教材を出力させることを目指します。
なお、上記の形式は実用英語検定・通称英検の一級・準一級のリスニングPart2形式に則っています。会話形式でなく一人の話者によって発話されるため、生成が容易であり英語力向上にも寄与しやすいと考えるためです。

他には、OpenAIのAPIとLangChainを利用した経験があること、Google Colabノートブックの取り扱いが全く初めてではないことを前提にします。

# 工夫した点
何回も機械とやり取りしてようやっと目標が達成できるブラウザのChatGPTはあまり好きではありません。ですので、Pythonを使用してAPIを叩きできるだけ機械とやり取りをしなくて済むように下記のような工夫をしました。
* Google Colabノートブックを使用した
* Playground上で試行錯誤して予めプロンプトを準備した
* JSON形式で回答を得られるようにした
* Pydanticを利用したクラスを作りJSONを準備する手間を省略した
* Few-Shot Promptingで出力の改善を試みた

JSONで回答を得ると、後で出力をごにょごにょするときにほしい部分だけ取り出せたり、コンテンツの内容と形式を安定させたりする効果があります。

能書きはこれくらいにして、やっていきましょう。

# コード
まずおまじないです。
```
!pip install openai langchain pydub
```
APIキーをシークレットから読み込んでおきましょう。
ついでに必要な設定も行なっておきます。Temperatureは割と直感で決めました。もう少し改善の余地はあるかもです。モデルはgpt-4でいいと思います。topicには問題文のテーマを入れます。たとえば、'modern politics'でや'European medieval history'だとかなんでも良いと思います。後ほど出てくるプロンプトを少しいじると、topicを指定しないこともできます。その場合、意外な題材が選ばれたりします。タクシーの歴史なんてのも生成されて面白かったです。また、英語の試験では絶対でないようなキワどい題材も扱えるのが嬉しいですね。
```python
from google.colab import userdata
import os,random

os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
OPENAI_API_MODEL = 'gpt-4'
OPENAI_API_TEMPERATURE = 0.8
topic = "Communism"
```
次に、Pydanticを使用して出力用のクラスを用意します。出力はJSONとなりますので、パーサーもついでに用意しておきます。
クラスの中身について少し補足しておきます。Questionクラスはまさしく問題を格納します。question_textに問い（What is blahblah?みたいな）、optionsに選択肢、あとは正答とその解説です。これをリストの中身として持つのがExamQuestionクラスで、本文と共にこのクラスを構成します。
```python
from typing import List
from pydantic import BaseModel
from langchain.output_parsers import PydanticOutputParser

class Question(BaseModel):
    question_text: str
    options: List[str]  # 文字列のリストとして定義
    correct_answer: str
    explanation: str

class ExamQuestion(BaseModel):
    passage_text: str
    questions: List[Question]  # Question クラスのインスタンスのリスト

parser = PydanticOutputParser(pydantic_object=ExamQuestion)
```
プロンプトを準備します。OpenAIのプラットフォームに用意されているPlaygroundで色々試行錯誤したものを貼り付けました。PlaygroundのUI上で、右上にあるView Codeというボタンを押すとAPIを叩く際のコードを出力してくれます。`message=[...]　`という部分がありますので、括弧内をコピーしてペーストしてします。

内容がわからない人に少しだけ説明すると、
* roleがsystemのところには、AIに演じてほしい役割
* userのところにはユーザーからのリクエスト
* assistantのところにはAIが生成した内容という想定（要はこういう風に生成しろよと明示できる）

といった内容がそれぞれ記載されています。

ここで、プロンプトは英文で記すとトークン数が少なくなり、お財布にとても優しいです。

user, assistantのプロンプト部分に共通する点ですが、出力の形式を担保するためJSONの形式を厳密に定めています。これをやらないと、出力の一部が足りない・間違っていることが頻発し、ブラウザのChatGPTよろしく後から「あれも出力して」「形式をああして、こうして」と何度も機械とやりとりするハメになってしまいます。

userのプロンプト部分の一部には、PydanticOutputParserのメソッドであるget_format_instructionの中身を使用しています。プロンプト部分もLangchain使ったらええやん、とお考えになるかもしれませんが、Playgroundからそのままコードをコピペしたいので、この部分はLangchainを使用しませんでした。

assistantのプロンプト部分は、旺文社が刊行した『2023年度版　英検１級過去6回全問題集』から引用したリスニング問題になります。本文、問い、選択肢、解説全て引用しましたので、改変等はしていません。ここをちょちょっといじれば任意のレベル・形式の問題を出力できると思います。

```python

#PlaygroundからView Codeしてそのまま貼り付ける
playground_viewcode=[
  {
    "role": "system",
    "content": "As a English teacher who experienced in teaching Japanese students and knowledgeable about their English proficiency levels, create a script of about 250 words, related to the topic of " + topic + ". This script should be suitable for Part 2 of the Listening section of the Eiken Test, Grade 1, a widely recognized English proficiency test in Japan. After the script, include two related questions. The script's subject matter should cover areas including, but not limited to science, nature, society, history, and politics, incorporating vocabulary appropriate for Eiken Test Grade 1. Title the script succinctly, using no more than four words. Present the content in a clear, direct manner, without an opening greeting or a lecture-style format. \n\n\n\n\n\n"
  },
  {
    "role": "user",
    "content": "Please generate the instructed content in system.  The output should be formatted as a JSON instance that conforms to the JSON schema below.\n\nAs an example, for the schema {\"properties\": {\"foo\": {\"title\": \"Foo\", \"description\": \"a list of strings\", \"type\": \"array\", \"items\": {\"type\": \"string\"}}}, \"required\": [\"foo\"]}\nthe object {\"foo\": [\"bar\", \"baz\"]} is a well-formatted instance of the schema. The object {\"properties\": {\"foo\": [\"bar\", \"baz\"]}} is not well-formatted.\n\nHere is the output schema:\n```\n{\"properties\": {\"passage_text\": {\"title\": \"Passage Text\", \"type\": \"string\"}, \"questions\": {\"title\": \"Questions\", \"type\": \"array\", \"items\": {\"$ref\": \"#/definitions/Question\"}}}, \"required\": [\"passage_text\", \"questions\"], \"definitions\": {\"Question\": {\"title\": \"Question\", \"type\": \"object\", \"properties\": {\"question_text\": {\"title\": \"Question Text\", \"type\": \"string\"}, \"options\": {\"title\": \"Options\", \"type\": \"array\", \"items\": {\"type\": \"string\"}}, \"correct_answer\": {\"title\": \"Correct Answer\", \"type\": \"string\"}, \"explanation\": {\"title\": \"Explanation\", \"type\": \"string\"}}, \"required\": [\"question_text\", \"options\", \"correct_answer\", \"explanation\"]}}}\n```"
  },
  {
    "role": "assistant",
    "content": "{\n   \"passage_text\": \"Sleep and Dementia. Researchers in France have been studying possible causes of dementia, a condition in which a person experiences a severe loss of cognitive function. They investigated whether the amount of sleep a person gets in middle age plays a role in the development of the condition. Using self-reported sleep data from a large-scale lifestyle survey, the researchers looked at 8,000 participants who had been research subjects since 1985. The team found that, compared to participants who reported sleeping seven hours per night in their 50s and 60s, those who regularly got six hours of sleep or less per night were 30% more likely to develop dementia in later decades. While this finding suggests that insufficient sleep could be a contributing factor in the development of dementia, it is not conclusive. It could be that lack of sleep in middle age is actually an early symptom of dementia, rather than a cause. However, a neurologist who commented on the study thinks this is probably not the case, as the poor sleeping habits arose such a long time before any of the subjects were diagnosed with dementia. Additionally, she points out that the first biological change that leads to dementia — the buildup of certain proteins in the brain — typically does not begin that early on\",\n   \"questions\": [\n       {\n           \"question_text\": \"What is one thing the researchers in France examined?\",\n           \"options\": [\"1 Evidence that shows people with dementia sleep more.\", \"2 Various sleep disorders reported by young people.\", \"3 Lifestyle changes as a treatment for dementia.\", \"4 The sleep patterns of middle-aged people.\"],\n           \"correct_answer\": \"4\",\n           \"explanation\": \"第1段落前半に、フランスの研究者たちが中年期の睡眠時間が認知症の発症にどのような役割を果たすかを調査したとあります。そして、50代と60代で一晩に7時間睡眠した参加者と比べて、6時間以下の睡眠をとっていた参加者は後の年代で認知症を発症するリスクが30％高かったという結果が示されています。これに基づいて、正解は「4. 中年期の人々の睡眠パターン」です。\"\n       },\n       {\n           \"question_text\": \"What does the neurologist believe?\",\n           \"options\": [\"1 Protein buildup is not always a sign of dementia.\", \"2 Participants' sleep patterns were likely not caused by dementia.\", \"3 Better treatments for dementia will soon be developed.\", \"4 Middle-aged people generally have difficulty sleeping.\"],\n           \"correct_answer\": \"2\",\n           \"explanation\": \"第2段落に、ある神経学者が中年期の睡眠不足が実際には認知症の初期症状ではないと考えていると述べられています。これは、睡眠不足の習慣が認知症の診断よりもずっと前に始まっていたこと、そして認知症につながる脳内の特定のタンパク質の蓄積がそれほど早く始まることは通常ないという点に基づいています。そのため、正解は「2. 参加者の睡眠パターンはおそらく認知症によるものではない」となります。\"\n       }\n   ]\n}\n"
  },
  {
    "role": "assistant",
    "content": "{\n   \"passage_text\": \"American Camels. Today, camels are an iconic symbol of the Middle East and North Africa. Surprisingly, however, evidence indicates that they originated in North America. Fossils suggest that the last indigenous camels in North America died out many thousands of years ago. Then, in the mid-1800s, the US government imported several dozen camels to deliver military supplies to remote areas. It was thought that the animals would be well-suited to the harsh desert climate of the southwestern United States. Indeed, initial field exercises were so successful that a high-ranking official suggested acquiring an additional thousand camels. When the American Civil War broke out shortly after the camels had been put to work, however, the plan was abandoned. Some of the camels that had been brought over were sold to private businesses such as circuses and mining operations for their value as entertainment and labor, but a few were released and continued living in the desert. Although there were several wild-camel sightings over the following decades, their numbers were too few to create a stable population, and it is thought they disappeared completely. Now, over 100 years later, some scientists are considering reintroducing camels a second time.\",\n   \"questions\": [\n       {\n           \"question_text\": \"Why did the US government import camels in the mid-1800s?\",\n           \"options\": [\"1 To supply goods to isolated regions.\", \"2 To improve relationships with foreign countries.\", \"3 To restore the ecology of desert areas.\", \"4 To give them as gifts to senior military officers.\"],\n           \"correct_answer\": \"1\",\n           \"explanation\": \"第1段落中ほどに、米国政府がラクダを輸入したのは、遠隔地域への軍事物資の輸送のためだったと書かれています。このことから、選択肢1「孤立した地域への物資の供給」が正解です。\"\n       },\n       {\n           \"question_text\": \"What do we learn about the camels that were released into the wild?\",\n           \"options\": [\"1 They were captured to be sold to circuses.\", \"2 They eventually spread across the desert.\", \"3 Their population was too small to sustain itself.\", \"4 They continued to be sighted for nearly 100 years.\"],\n           \"correct_answer\": \"3\",\n           \"explanation\": \"第2段落の最後に、野生に放されたラクダは、安定した集団を形成するには数が少なすぎたと述べられています。そのため、選択肢3「彼らの集団は自己維持するには小さすぎた」が正解です。\"\n       }\n   ]\n}\n"
  }
]
```
次にPlaygroundからコピペしたmessageをLangChainで扱えるオブジェクトに変換します。なんか面倒なことしていますが、LangChainを使ってよしなにやってくれないのだろうか、ご存知の方がいましたらご一報ください。
```python
# OpenAIのPlayground, view Codeからコピーしたmessage部分をLangchainベースに変換する関数を定義
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    )

def format_messages(original_messages):
    formatted_messages = []
    for msg in original_messages:
        if msg['role'] == 'system':
            formatted_messages.append(SystemMessage(content=msg['content']))
        elif msg['role'] == 'user':
            formatted_messages.append(HumanMessage(content=msg['content']))
        elif msg['role'] == 'assistant':
            formatted_messages.append(AIMessage(content=msg['content']))
    return formatted_messages

messages = format_messages(playground_viewcode)
```
さて、次はいよいよ生成に移ります。LangChainを使うとコードがスッキリしていいですね。Audio関数はその場で音声を聴きたい時に活用すると良いです。
outputにAPIからの返答を入れておきます。これはJSONオブジェクトになっているので、`output.content`で肝心の部分にアクセスできます。さらにこれをパースして各部分をそれぞれ取り出せるようにしておきましょう。

取り出したものには早速、変数名をわかりやすくつけておきます。
```python
from openai import OpenAI
from langchain.chat_models import ChatOpenAI
# from IPython.display import Audio

chat = ChatOpenAI(model_name=OPENAI_API_MODEL, temperature=OPENAI_API_TEMPERATURE)

output = chat(messages)
exam_question = parser.parse(output.content)

listening_passage_text = exam_question.passage_text
listening_Q1 = exam_question.questions[0].question_text
listening_Q2 = exam_question.questions[1].question_text
```
ここまでくれば後少しです。得られたテキストを音声にしていきましょう。まずはGoogle Driveをマウントして、フォルダのパスを書いておきます。マウントしますか？と聞かれたらもちろん許可してください。
```python
from google.colab import drive
drive.mount('/content/drive')

# 対象のフォルダパス
folder_path = '/content/drive/My Drive/'
```
OpenAIのText to Speechには、6人の音声が用意されています。今回はこの6人の名から二人をランダムに選んでしゃべらせます。ちなみにonyxは声がこもっている感じで聞き取りづらいです。

つづけて音声にしましょう。音声化する際に必要な引数は下記のとおりです。
* input: 文字通りインプット、音声化したいテキストです。
* model: tts-1とtts-1-hdの二つがあります。前者の方が安く、後者の方が高品質らしいです。が、私の耳にはあまり違いを感じられません。安い方の前者で十分だと思います。
* voice: 誰の声を使うかです。今回は利用できる6人の中からランダムで選択しています。
* speed: 音声の速さです。早口or遅口にするというよりも、音声ファイル自体の再生速度をいじっているようなので、早すぎたり遅すぎたりすると違和感がでます。個人的にはデフォルトの1.0を推奨します。
```python
import random

names = ["alloy", "echo", "fable", "nova", "onyx", "shimmer"]
selected = random.sample(names, 2)
name1, name2 = selected[0], selected[1]

client = OpenAI()

sound_file = client.audio.speech.create(input=listening_passage_text, model="tts-1", voice=name1, speed=1.0)
sound_file.stream_to_file(folder_path + "passage.mp3")
sound_file = client.audio.speech.create(input=listening_Q1, model="tts-1", voice=name2, speed=1.0)
sound_file.stream_to_file(folder_path + "q1.mp3")
sound_file = client.audio.speech.create(input=listening_Q2, model="tts-1", voice=name2, speed=1.0)
sound_file.stream_to_file(folder_path + "q2.mp3")
```
ここまでで問題としての体裁が整いました！！！早速聞いてみましょう！！！問いの音声と選択肢は本文のツイートに連なっているぞ。聴きたい人は開いてみてくれ。ついでにフォローとかしてくれちゃうとうれしい。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ChatGPTを駆使して英検のリスニング問題を作った件。準一級から一級レベルかも。 <a href="https://t.co/vSmoitMhyL">pic.twitter.com/vSmoitMhyL</a></p>&mdash; Kaz (@kaz_yatteiki) <a href="https://twitter.com/kaz_yatteiki/status/1739491028745490783?ref_src=twsrc%5Etfw">December 26, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

問題解けましたか？では中身のテキストを確認しましょう。（printがいっぱい並んでいてなんだか気持ちわるい……。）
```python
print("\n"+exam_question.passage_text+"\n")
print('問１'+"\n")
for items in exam_question.questions[0].options:
  print(items+"\n")
print('問２'+"\n")
for items in exam_question.questions[1].options:
  print(items+"\n")

print("Question 1")
print(exam_question.questions[0].question_text+"\n")
print("Question 2")
print(exam_question.questions[1].question_text+"\n")

print("問１　正解　" + exam_question.questions[0].correct_answer+"\n")
print(exam_question.questions[0].explanation+"\n")
print("問２　正解　" + exam_question.questions[1].correct_answer+"\n")
print(exam_question.questions[1].explanation+"\n")
```
Understanding Communism. Communism, a political thought that has shaped world history, is derived from the works of Karl Marx and Friedrich Engels. It advocates for the establishment of a classless society where goods and resources are collectively owned. This political philosophy asserts that societies progress through a process of class struggle, especially between owners of resources, known as the bourgeoisie, and workers, known as the proletariat. The theory holds that this class struggle would eventually lead to a revolution, resulting in the overthrow of the bourgeoisie and the establishment of a proletarian state. However, in practice, communist societies often resort to totalitarian regimes to enforce the collective ownership of resources, leading to human rights abuses. The downfall of the Soviet Union, the world's first communist state, is often cited as an example of the limitations of communism. Despite its issues, it continues to significantly influence global politics today, as seen in countries such as China and Cuba.

問１

1 To create a class-based society.

2 To establish a classless, stateless society.

3 To promote private ownership of resources.

4 To create authoritarian regimes.

問２

1 To prevent societal division and inequality.

2 To ensure the transition from capitalism to communism.

3 To stray from the ideals of a classless society.

4 To promote corruption and rights abuses.

Question 1
What is the ultimate goal of Communism?

Question 2
Why do leaders of communist regimes argue for a strong state?

問１　正解　2

The first sentence of the passage indicates that the ultimate goal of Communism is the establishment of a classless, stateless society.

問２　正解　2

The passage mentions that leaders of nations (such as the former Soviet Union and present-day North Korea) argue that a strong state is necessary to ensure the transition from capitalism to communism.


問題の解説部分に関してはなかなか安定しないことが多く、英語で出力されることもしばしばです。10回中7回は日本語で出てきます。
そして英語中級者までの人には少し難しかったかも。。。難易度はプロンプトでよしなに調整できるはずなので、色々試してみてください。
では最後に難しい単語の解説を出力して終わりにしましょう
```python
chat = ChatOpenAI(model_name=OPENAI_API_MODEL, temperature=OPENAI_API_TEMPERATURE)
words_explanation = exam_question.passage_text + 'Please list the English words in this sentence that you think are difficult enough to appear in Grade 1 of the EIKEN test. In doing so, make sure that the first letter of each word is in lowercase. Please also provide their pronunciation, meaning, example sentences, etymology, and any other tips that make them easier to remember. Please provide the output in Japanese.'
output_words = chat([HumanMessage(content=words_explanation)])
print(output_words.content)
```

1. communism
[ˈkɑːmjəˌnɪzm]
意味：共産主義。社会主義の一形態で、財産や生産手段を全ての人々が共有することを主張する。
例文：Communism was the political system in the Soviet Union.
語源：フランス語のcommunismeから起源し、それ自体はラテン語のcommunis（共通の）からきています。
Tips：community（共同体）と同じcomの部分を覚える。

2. bourgeoisie
[bʊrʒˈwɑːzi]
意味：ブルジョワジー、中産階級。カール・マルクスはブルジョアジーを資本主義社会の支配的経済階級と定義した。
例文：The bourgeoisie were accused of exploiting the proletariat.
語源：フランス語のbourgeoisie、中産階級や商人クラスを指す。
Tips：ブルジョワ（bourgeois）というフランス語の言葉に基づいています。ブルジョワは、町の自由市民を意味する。

3. proletarian
[ˌproʊlɪ'tɛəriən]
意味：労働者階級の人々。資本主義社会における生産手段を持たない労働者階級を指す。
例文：The proletarian revolution was a key idea in Marx's theory of Communism.
語源：ラテン語のproletarius（貧しい市民）から派生。
Tips：Proletariat（労働者階級）の形容詞形。

4. totalitarian
[toʊˌtælɪ'terɪən]
意味：全体主義的な、全体主義の。政府が一方的に全ての社会活動を統制しようとする政治体制を指す。
例文：The government became more totalitarian after the coup.
語源：イタリア語のtotalitario（全体的な）から
Tips：Total（全体の）＋-itarian（...主義の）を組み合わせた造語。全体を統制するという意味。

5. regime
[riːˈʒiːm]
意味：体制、政権。特に、一定の政策、原則、運営方法を持つ政府や組織。
例文：The current regime has been in power for ten years.
語源：フランス語のrégime （支配、政府）.
Tips：軍事政権・体制などによく使われる言葉です。"Regime change"は「政権交代」を意味します。

# おわりに
お疲れ様でした。これであなたの英語力は今日から爆上げのはずです。いくつか課題や改善できる点を書いておきます。
* topicを指定すると、一般常識で解ける問題を生成することが多くなりがち。中学高校で学習した内容から答えが推測できてしまい、リスニングの問題としてはイマイチ。
* 指定しないと、今度はやたらと量子力学や森林破壊の問題を生成する。もうええってってなりがち。
* でも指定していない場合、たまーに結構面白いトピックを生成する。例えばロンドンでビーバーが果たした役割や、タクシーのイノベーションの歴史など。タクシーにおける無線通信って実はとんでもないイノベーションだったんですね、言われてみればわかりますが普通はなかなか気づかないです。
* 難易度が安定しない？1級レベルを意図しましたが、語彙レベルが準1級程度となることがしばしばある。
* まとめると、Fine-tuningをもっと頑張る必要がありそう。

以上でございます。それでは皆さんも生成AIを活用して自分の生活を豊かにしていってください。