見出し画像

【解説】全自動でPDF問題集を作成 ~生成AI x Pythonで問題集を生成 VOICEVOXでオーディオブックも~

こんにちはRcatです。
本当はすぐに解説を出す予定だったんですが、ぽっと出のアイデアであるオーディオブックが割り込んでしまい、遅くなってしまいました。
こちらの記事ではソースコードなどを交えながら解説などをしていこうと思います。

※音声認識を使っているので、解答と回答が間違っていたりしますが、あまり気にしないでください。

初回はこちら

オーディオブックはこちら



はじめに

利用規約

情報や作品の活用時は事前に利用規約をご確認ください。

コメントについて

利用規約のガイドラインを確認の上コメントしてください


全体のソースコード

本記事では解説部分だけを抜粋して表示します。
全体につきましては、事前にダウンロードをしてください。
初回の記事より配布しております。


処理の流れ

処理の流れ

まずはざっと本ツールの処理の流れを説明しておきます。
まずはこちらをご覧ください。

本ツールは入力データとして試験範囲を用います。G検定であれば公式のシラバスが一番分かりやすいです。
項目、目標などが書かれたファイルにプラスキーワードという形です。
生成AIはランダムと言っても再現性があるのでランダムにはなりません。そのためこちらからキーワードを指定するような仕組みにしています。
そうすることで、キーワードに沿った問題を生成せざるを得ない状況を作ります。

次に問題の生成です。
入力された試験範囲を元に試験問題を生成します。
なお、ハルシネーション対策のために一度フィードバックを噛ませる形にしています。

出来上がった試験問題はJSONデータで後処理に渡ります。
後処理では3つの出力方法のいずれかで出力を行います。


Web受験ページ

これは本ツールを立ち上げている時のみ利用可能なものです。この後紹介するPDF出力の画面レイアウトを得るためにHTMLを使用しております。ですので、せっかくならリアルタイムで問題を解ける環境を用意してもいいのではないかと思い用意したものです。

PDF化 電子書籍化

問題をHTMLとCSSを使い画面上にレイアウトします。その結果を印刷するような形でPDF化を行います。
事前に問題数を入力することで、まとめて数百ページ分の問題集を作成することが可能です。
また、PDFモードではページ数がキーワードよりも多い場合は、必ず一度は使われるようにキーワードの選出方法が1周モードに変更されます。

電子書籍で問題と回答を行ったり来たりしないですむように、1問1答形式なのが特徴です。

読み上げ オーディオブック化

問題生成時にオーディオブックに特化した形でテキストを出力し、ボイスボックスを使って読み上げます。
各問題をバラバラで出力するか、まとめて1つのファイルにするか選ぶことができます。
満員電車とかでも聞くだけならできますよね。
まあ、途中の選択肢の頃には最初の選択肢を忘れてるのが私なんですが、


試験範囲データ

試験範囲の読み込み

ではまず最初のステップ、試験範囲データについて解説。
試験範囲はタブ区切りのtsvファイルで作成します。
理由としてはキーワードを入力する時にカンマ区切りで入力したかったからです。だからカラムのデリミタを犠牲にしたわけですね。

最初の記事の方にも貼りましたが、こんな感じで作成します。
カラムは自由ですが、必ずキーワードという列を含めます。そして、キーワードの列だった場合はカンマ区切りでキーワードを列挙します。

一部を抜粋するとこんな感じ

大項目	中項目	目標	キーワード
人工知能をめぐる動向	3.探索・推論	・探索,・推論の具体例を説明できる,・探索,・推論を実現する代表的な手法を説明できる	αβ法,Mini-Max法,SHRDLU,STRIPS,探索木,ハノイの塔,幅優先探索,深さ優先探索,ブルートフォース,モンテカルロ法

そして、キーワードを読み込む関数がこちら。
前提として試験1つにつき、1つのオブジェクトとなるようにクラスが作成されています。ですので、読み込むと結果はインスタンス変数に代入されます。

このクラスはインスタンス時にどのフォルダを使うかを渡されます。そして、フォルダの中のファイルを検索して読み込み仕組みです。なので実は拡張子なんでもいいです。逆に変なファイルが入っても読み込もうとするので、変なものは入れないでください。
読み込みにはCSVライブラリを使っています。
デリミタを明示的にタブにすることで、タブ区切りのファイルを読み込むことができます。
そして、キーワードという名前のヘッダーを探し、それと同じインデックスだった場合はキーワードとして読み込むという流れになります。

こういうやり方をしているので、ファイルの自由度は高くキーワードという列がある以外は縛りはありません
なので、AIが正しく認識できないようなキーワードがある場合は補足といった列を追加することが考えられます
例えばこういうのがあるんですが、単純に"暗号的強度"というキーワードの場合、暗号化アルゴリズムのことだと認識して、どういう暗号化アルゴリズムが強いですみたいな問題になってしまいます。
間違いではないですが、この場合言いたいことはモジュール強度の話なので、暗号のように意味がわからない関係性という意味で使われています。
こういった場合は補足をつけ足すことでこのように改善する場合があります。

ちなみに大元のオブジェクトの生成はこんな感じ。
ロードというのは、グローバルの関数で一番最初に呼び出されています。
読み込んだオブジェクトは辞書に代入することで、試験名で参照できるようになります。

問題を生成する

問題の生成

では、次に問題の生成について見ていきましょう。
問題を生成するにはいくつか方法があるのですが、今回は1番簡単なウェブ画面での表示を使って解説していきます。
なお、AIのモデルはGoogleのGeminiを使用しています。

まず、Web画面での問題生成関数がこちら
この関数は問題を解くURLにアクセスされた場合に呼び出される関数で試験名をパラメーターに取ります。

そしてCreateAIMondaiというあからさまの名前の関数を使うことで問題を生成します。この関数は出来上がった問題を専用のオブジェクトで返します。

問題生成前半

というわけで大元のCreateAIMondai関数の方を見ていきましょう。
この関数はかなり長いので、前半と後半に分けましょう。

まずは前半です。
この関数は試験名とチェックとひらがなのフラグを引数として取ります。

いきなりクラスの定義が入っています。先ほどオブジェクトを返すと言いましたが、このクラスのオブジェクトを返します。これはウェブページで回答を表示する時にIDを保持したい都合上をこうなっています。

ちなみにクラス定義が終わった後はこの3行しかありません。
オブジェクトを生成して試験試験範囲を入力してチェックフラグを渡すだけです。関数内にクラスを定義してるので、その辺の引数を何か使っちゃってんのは見逃して…。だってめんどくさいんだもん。

クリエイト関数は問題を生成して辞書で返します。
辞書で返す理由は問題文や選択肢を分けておくとレイアウトしやすいからです。
プロンプトにつきましては、ここでは非公開とさせていただきます。

試験範囲の抽出

大事なのはプロンプトの直前のメソッド呼び出しになります。実はこれ一番最初に読み込んだ試験範囲オブジェクトのメソッドになります。
効果は試験範囲の一部を文字列として返す関数です。

その中身がこちらになります。

ここは地味にこだわったところでもあります。まず、最初にキーワードを取得します。ランダムですが、少し補正をかけたランダムになります。
次に試験範囲からN%の行を抜き出します。
そしてキーワードがある行がランダムに選ばれるまで無限にループします。
こんな変なことをしている理由としては、少しでもAIに入力するデータをばらつかせたいことです。

全体のN%を抜き出した時、キーワードがあるようが一番上なのか一番下なのかで周辺の文字列が変わってきますよね。
同じ文字列を入力するとAIは再現性があるので、どうしても同じような問題を作りがちです。なので、せめてもの悪あがきとして、試験の入力範囲がばらつくようにしています。その結果なんか長い関数になっちゃってるんですね。

キーワードの抽出

ちなみにキーワードを選ぶ部分がこちら。
なんとめっちゃ行数があります。ランダム1行じゃないんですね。

何やってるかと言うと試験問題なのでランダムでは出てきて欲しいんですが、まんべんなく出てきてほしいです。
なので、実は抽選履歴というリストがあり、履歴の先頭にあるものが0%最後にあるものが100%という扱いで、キーワードを選んだ後に抽選にかかります。こうすることで、直近のキーワードがドロップしやすくなり、まんべんなく出やすくなるような形にしています。

なお、PDFで問題集を作る時は全てのキーワードを必ず使いたいので、一度使ったキーワードは出なくなるような一周モードというのも搭載しています。この場合はキーワードを使い切ると全部復活する仕様です。

問題生成後半

さて、試験範囲を抽出するところを確認したところで後半です。というか長いですね…。

後半戦はプロンプトの後に先ほど抽出した試験範囲をくっつけた状態でAIに丸投げします。
AIにはJSON形式で返すように指示してあるので、指示通りに働いてくれれば辞書として取得することができます。そうでない場合は何かしらエラーということでやり直しになります。

次にチェックです。
チェックが有効な場合は、もう1度AIを使って中身を確認します。多段階推論が得意なモデルであればいらないかもしれませんが、少なくとも現段階のGeminiディープシンクはどこかよくわからない国の言葉を混ぜて応答してくる謎モデルなので現状使えません。というわけで諦めて3段階にします。

チェックを行うプロンプトに対して前回の結果を貼り付けてまたも丸投げします。
指摘がない場合は空欄で返すように指示しているので、指摘があった場合はトリプルorの分岐でもう1回生成することになります。
この時は最初の出力と指摘の内容を再生成のプロンプトにくっつけて丸投げします。それで出てきた結果は治っていると信じましょう。

そうでない場合?人間も100%じゃないんだから許容しましょうよ。
そもそも本作に頼りきった試験勉強は推奨しておりません。試験問題の水増しとして使ってください。

ハルシネーション対策

先ほどのチェックと再生成ですが、ハルシネーションと呼ばれる現象の対策です。

ハルシネーションとは息をするようにAIが嘘をつく現象のことです。
端的に言ってしまえば、AIは次に来る単語予測し続けることで文章を生成しているので、それ本来の意味を理解しているわけではありません。もちろん注意機構など、単語の関連性に注意を払う機構は搭載されていますが、だからといって本質的な意味で理解してるわけではないということです。
なのである言葉が予測されたその次の単語の予測結果が、今までの内容と矛盾することがあります。端的に言えばこれがあるハルシネーションになります。

適当だけど、こんな感じのイメージ。可愛い90%は独断と偏見。
"ねこもふもふです"なら意味は通りますが、"猫もふもふ癒し"だとちょっと変ですよね。
実際はこうならないように工夫が施されていますが、どうしても単語の関係を捉えきれないことがあります。その結果、変な出力が生まれることがあります。

で、どうするかですが、もう1回AIを噛ませてチェックします。敵対生成ネットワーク的な感じですね。確かに本質的に理解していませんが、文章を読み解く力はかなりあるので、間違った文章を入力すれば間違っていると指摘することができます
その上で再度生成をかけるとかなりの確率で訂正することが可能です。

つまり2回AI使うんでちょっと遅いです。フィードバックがあった場合は3回AIを使うことになります。
Geminiにはいくつかモデルがあるので、初回はFlashチェックにLiteを使って、再生成はProを使って等、目的とパフォーマンスに応じて変化させるのがおすすめ。

実際に間違いが含まれた問題文や回答が生成された時、その違いが指摘され、修正されるという場面を何度か目撃しています。

例えばこんなことがありました

2025-03-12 14:15:33,997 ERROR 問題集作成サーバー 問題チェックで指摘が発生
2025-03-12 14:15:33,997 ERROR 問題集作成サーバー 元のデータ: {
  "problem": "以下の選択肢の中で、学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法はどれですか?",
  "choices": "1. ファインチューニング\n2. One-shot\n3. zero-shot\n4. Few-shot",
  "answer": 3,
  "commentary": "正解は3です。問題のキーワードはzero-shotでした。\n\n各選択肢の解説は以下の通りです。\n\n1. ファインチューニング\n事前学習済みのモデルを、特定のタスクのデータセットで再学習させる手法です。事前学習で得られた知識を活かしつつ、タスクに特化した調整を行います。\n\n2. One-shot\n少数のデータ(通常は1つ)のみを使用して学習を行う手法です。新しいタスクに対して、事前に学習した知識を適用し、少ないデータで高い精度を目指します。\n\n3. zero-shot\n学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法です。例えば、画像認識モデルが学習時に見たことのないカテゴリの画像を認識できるようにします。\n\n4. Few-shot\nごく少数の教師データを用いてモデルを学習させる手法です。zero-shotよりは教師データを使用しますが、通常の教師あり学習よりも少ないデータで学習を行います。\n\nキーワード説明:\nzero-shot(ゼロショット)とは、機械学習の分野で、学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法です。これにより、新しいタスクや未知のデータに対して、データ収集やアノテーションのコストを削減しつつ、高い汎化性能を発揮することが期待されます。",
  "keywd": "zero-shot",
  "kind": "キーワードを選ばせる問題"
}
2025-03-12 14:15:33,998 ERROR 問題集作成サーバー 指摘内容: {
  "problem_comment": "問題文がやや冗長で、何を問うているのか分かりにくい可能性があります。「学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法」という部分が、そのままzero-shotの説明になっているため、もう少し抽象的な表現にするか、別の角度から問う方が良いかもしれません。\n\n修正案の例:\n「以下の選択肢の中から、学習データなしで新たなタスクを実行できる機械学習の手法はどれですか?」",
  "choices_comment": "",
  "commentary_comment": ""
}
2025-03-12 14:15:36,608 ERROR 問題集作成サーバー 修正後: {
  "problem": "以下の選択肢の中から、学習データなしで新たなタスクを実行できる機械学習の手法はどれですか?",
  "choices": "1. ファインチューニング\n2. One-shot\n3. zero-shot\n4. Few-shot",
  "answer": 3,
  "commentary": "正解は3です。問題のキーワードはzero-shotでした。\n\n各選択肢の解説は以下の通りです。\n\n1. ファインチューニング\n事前学習済みのモデルを、特定のタスクのデータセットで再学習させる手法です。事前学習で得られた知識を活かしつつ、タスクに特化した調整を行います。\n\n2. One-shot\n少数のデータ(通常は1つ)のみを使用して学習を行う手法です。新しいタスクに対して、事前に学習した知識を適用し、少ないデータで高い精度を目指します。\n\n3. zero-shot\n学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法です。例えば、画像認識モデルが学習時に見たことのないカテゴリの画像を認識できるようにします。\n\n4. Few-shot\nごく少数の教師データを用いてモデルを学習させる手法です。zero-shotよりは教師データを使用しますが、通常の教師あり学習よりも少ないデータで学習を行います。\n\nキーワード説明:\nzero-shot(ゼロショット)とは、機械学習の分野で、学習時に明示的な教師データを使用せずに、学習済みのモデルの知識を別のタスクに適用する手法です。これにより、新しいタスクや未知のデータに対して、データ収集やアノテーションのコストを削減しつつ、高い汎化性能を発揮することが期待されます。",
  "keywd": "zero-shot",
  "kind": "キーワードを選ばせる問題"
}

この問題では問題文があいまいであると指摘されています。
確かに"別のタスクに適用"という文章がなければ教師なし学習かなとも思ってしまう説明ですね。
そして、指摘するだけでなく新しい提案もされています。
一度に生成すると品質が落ちてしまいがちですが、この方法の場合は洗練することができますね。
そして、修正後のデータでは提案の方を受け入れた問題文に変わっています。
確かにこの説明の方が問題集としてしっくりきますね。

似たような形で、2つの選択肢が似ていて、どっちを選べばいいかわからないような状態を訂正したり、解説の中の文章が間違っているところを訂正したりすることがあります。

出来上がった問題データ

ちなみに出来上がった問題を一部公開してしまうとこんな感じになります。
問題と選択肢、そして解説が別れて辞書に入ってきます。
この後の下流工程ではこれを利用していきます。

{
  "problem": "DALL-Eのような、テキストによる指示に基づいて画像を生成するAIモデルは、マルチモーダルタスクの何と呼ばれる分野に該当しますか?",
  "choices": "1. Visual Question Answering\n2. Text-To-Image\n3. ImageCaptioning\n4. Unified-IO",
  "answer": 2,
  "commentary": "正解は2です。\n\n各選択肢の解説は以下の通りです。\n\n1. Visual Question Answering\n\n画像(Visual)を見て、それに対する質問(Question)に答える(Answering)タスクです。DALL-Eは質問に答えるのではなく、画像を生成するので該当しません。\n\n2. Text-To-Image\n\nテキスト(Text)から画像(Image)を生成するタスクです。DALL-Eはテキストによる指示に基づいて画像を生成するため、Text-To-Imageに該当します。\n\n3. ImageCaptioning\n\n画像(Image)の内容を説明するキャプション(Captioning)を生成するタスクです。DALL-Eは画像を生成するので該当しません。\n\n4. Unified-IO\n\n複数のモーダル(テキスト、画像、音声など)を入力・出力として扱うモデルの総称です。DALL-Eはテキストを入力、画像を出力とするため、より具体的なText-to-Imageの方が適切な分類となります。\n\nDALL-Eはテキストで指示された内容に基づいて画像を生成するText-To-Imageモデルとして知られています。",
  "keywd": "DALL-E",
  "kind": "キーワードの説明を選ぶ問題"
}

後処理について

次に出来上がった問題を表示したり、読み上げたりする処理について見ていきます。

ウェブ画面の構築

まず、最初に上の例でも使ったウェブ画面での例です。この後のPDF化にも関連してきます。

先ほどもお見せしましたが、こちらがウェブ画面での問題表示関数です。
問題が生成された後、テンプレートを書き換える形で応答を作成します。
テンプレートとは中身の置換が可能なHTMLファイルです。

例えば問題表示画面をそのまま何もせずに読み込むとこんな感じになります。
上の関数と見比べてみてください。プレースホルダの中身と引数の名前が一致しているのが分かると思います。
セーフにしているのは改行をBRに置き換えないと表示上改行されないので、そうするためにセーフにしています。

この中身に問題が入ってくると以下のようになるというわけですね。

回答側がこちら
回答は前回生成した内容がIDと紐付けられてキャッシュとして保存されているので、それを読み込みます。

PDF出力の手順

では、次にPDFとして出力する場合を見ていきましょう。
PDFと指定出力する場合の関数がこちらになります。

この関数はパラメーターとして試験の名前とページ数を受け取ります。
特徴としては全てのキーワードを使うために一周モードに問題生成モードをチェンジするところです。こうすることでまんべんなく問題を作成することができます。
また、PDF化のために自作のPDFクラスをインスタンスしています。

PDFクラスは別の記事でも紹介した以下です。
2つのライブラリを組み合わせています。1つ目はHTMLをPDFに変換するライブラリです。これを使うことで、HTML表示そのものをPDF化することができます。
もう1つはPDFを繋げるライブラリです。全体を1つの長い、HTMLとして作ればページ分割もできるのかもしれませんが、今回は1ページずつ作成するので、ページごとのPDFを作った後、1つずつくっつけていくといった形です。
バイトIOで作っているので、ファイルへの書き出しは最後だけです。
これでPDF化は完了です。

出来上がったPDFはこういうのを使うと電子書籍として読むことができます。本作のページサイズはKindleのサイズで作ってあります。
https://www.amazon.co.jp/sendtokindle

オーディオブックに特化した処理

最後にオーディオブックを作る処理です。
どんなことができるのかに関しては、先にこちらを読んでいただくと理解が深まるかもしれません。

で、中身なんですが、これも結構長いので分割します。
まずオーディオを作成するには、このCGIにパラメーターを持ってアクセスする必要があります。
PDFの時に加えてキャラクターのID、話す速度、回答への移行時間、マージのフラグといった追加のパラメーターがあります。

そして音声特有のデータとして出力結果のパスと、ファイル全体の長さを保有しているところが特徴です。最後に結合しなければ全体長さはいらないんですが、結合しようとすると全体の長さが必要です。
そして、最後の2行は毎回使う音声を先に生成しています。ここで言うVVとは、自作のボイスボックスのAPIを叩くオブジェクトです。これに対してテキストなどの情報を入力するとバイトIO形式でWAVファイルが返ってきます。

さて、準備ができたらループで問題を生成します。問題の文章生成までは今までと同じです。
少し違うところはひらがな引数をTRUEにしてるところです。というのも音声合成ソフトはものによっては英語が全く読めません。特に英単語をアルファベットで言われたところで全く分かりません。ということで、音声の場合は全ての出力結果をひらがなで出力するようにプロンプトが変化します。

出力結果はこんな感じ。めちゃくちゃ読みにくいですが、音声として聞く場合はこれが最適です。

{
  "problem": "もでるのけいりょうかにおいて、むだなかそうやぱらめーたーをへらし、こんぱくとにもでるをえらぶほうほうはどれですか?",
  "choices": "1. じょうりゅう\n2. ぷるーにんぐ\n3. ぶんりょうか\n4. じこきょうしありがくしゅう",
  "answer": 2,
  "commentary": "せいかいは2ばんです。もんだいのキーワードはぷるーにんぐです。ぷるーにんぐは、もでるをちいさくし、けいさんりょうやめもりしようりょうをへらすぎじゅつです。",
  "keywd": "ぷるーにんぐ",
  "kind": "キーワードのせつめいをえらぶもんだい"
}

音声合成の特徴は区間ごとに音声の生成をしているところです。
具体的には「問題」+「問題の本文」+「選択肢」+「選択肢の本文」+「回答の本文」のようにいくつかに分割して作った音声を後で結合しています。

ここで最初に初期化しているwaveobjというのも自作のオブジェクトで、ある一定の長さを持つ空の音声ファイルを作成し、任意の秒数のところに挿入できるオブジェクトです。後からカットもできるので、まずは5分で生成してます。
それに対して、前の方の音声から順番に挿入していくことで、1つの音声ファイルとしています。
ちなみに最後の秒数を覚えているので、後ろから何秒残すというのを最初に指定しておけば、後は秒数管理しなくても大丈夫です。
出来上がったら必要なところだけカットして保存すればオーディオの完成

残りの後半の部分ですが、このようになっています。
マージしない場合は専用のフォルダを作ってまとめています。
マージする場合は全体の長さのファイルを作成し、順番に挿入していくことで1つのファイルとして出力しています。


まとめ

今回は全自動問題集生成の解説を行いました。
最初はPDFができればいいかななんて思ってたんですが、気づいたらなんかしっかりしたトップページとオーディオブック機能がついてました。何ででしょうね。
何と言うか、今までいろんなことやってると本当に思わぬところで役に立つもんですね…。
オーディオブックは完全に興味本位の機能ですが、PDF作成は今後も使えそうな技術なので、機会があればどんどん使っていきたいです。
もちろん勉強(主に単語を覚えなきゃいけないもの)については普通に使えると思うので生かしていきたいと思います。
それではまたお会いしましょう。


ソースコードの配布

以下の記事より配布しています。

プロンプトの解説については別途こちらで行います。


いいなと思ったら応援しよう!

Rcat999 情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。