レベル: 中級 Nicholas Chase (ibmquestions@nicholaschase.com), Writer, Freelance
2008年 2月 19日 この 2 回シリーズの記事では、E4X (ECMAScript for XML) と Prototype JavaScript ライブラリーの両方を使って Ajax による単純な読心術アプリケーション (20 の質問ゲーム) を作成する方法を学び、またその過程で新しいオブジェクトについて学びます。第 1 回ではこのアプリケーションのシステムを作成する方法を学びました。このシステムは既存のナレッジ・ベースを分析し、ユーザーが何を考えているかを判断します。この第 2 回では、ナレッジ・ベースに新しい情報を追加する方法と、Prototype JavaScript ライブラリーを使って Twenty Questions アプリケーションを外部のデータベースと統合する方法を学びます。こうすることで、1 人のユーザーがトレーニングした結果を、このゲームで遊ぶ他のユーザーが利用できるようにします。
完成したアプリケーションは http://www.backstopmedia.com/examples/e4x.html で見ることができます。この記事では、XML と JavaScript の概念について理解していることを前提とします。まだスキルを磨く必要がある場合には「参考文献」を参照してください。また、E4X 対応のブラウザー (Firefox 1.5 以上など) も利用できる必要があります。
前回の記事で説明した内容
まだ第 1 回を読んでいない人は、まずそれを読む必要があります (「参考文献」を参照)。第 1 回で作成したアプリケーションは、主に「ハイ (yes)」と「イイエ (no)」による質問を使ってナレッジ・ベースを分析し、ユーザーがどんなものを考えているのかを判断します。このナレッジ・ベースはリスト 1 のようなものです。
リスト 1. ナレッジ・ベースのサンプル
<knowledgebase>
<questions>
<question id='1'>
<display>Is it animal, vegetable, or mineral?</display>
<answerOption>Animal</answerOption><
answerOption>Vegetable</answerOption>
<answerOption>Mineral</answerOption>
</question>
...
</questions>
<targets>
<target id='1'>
<display>a house cat</display>
<answer questionid = '1'><answerValue1>Animal</
answerValue1></answer>
<answer questionid = '41'><answerValue41>No</
answerValue41></answer>
</target>
<target id='2'>
<display>a carrot</display>
<answer questionid = '1'><answerValue1>Vegetable</
answerValue1></answer>
<answer questionid = '44'><answerValue44>No</
answerValue44></answer>
</target>
...
</targets>
</knowledgebase>
|
 |
頻繁に使用される頭字語
- Ajax: Asynchronous JavaScript™ and XML
- DOM: Document Object Model
- HTML: Hypertext Markup Language
- JSON: JavaScript Object Notation
- XML: Extensible Markup Language
|
|
この分析を行うために使用するアルゴリズムでは、ある 1 つの質問をし、その答えに対応しないアイテムをすべて排除し、残ったアイテムに最も共通する質問を見つけ、その共通する質問を残りのアイテムが 1 つのみになるまで続けます。
残りのアイテムが 1 つのみになった時点で、このアプリケーションは、その最後のアイテムがユーザーが考えていたものだと言い当てます。答えが正しい場合には、アプリケーションは最初に戻ります。しかし答えが誤っている場合には、さらに作業をする必要があります。
答えが誤っている場合
答えの候補となりうる最後のアイテムにまでアプリケーションが到達し、そしてそのアイテムが正解ではなかった場合、ユーザーが実際に考えていたアイテムを (それがどんなアイテムであれ) システムに教える必要があります。例えば、もしユーザーが「celery (セロリ)」を考えていたとし、システムが「a carrot (人参)」と答えたとすると、システムは「celery (セロリ)」が存在することを知る必要があります。またシステムは「celery (セロリ)」と「a carrot (人参)」の違いを区別する方法も知る必要もあります。
最初の課題は、正しい答えが何であったかを知ることです。このフォームは単純です (リスト 2)。
注意: 第 1 回を読まなかった人は、「参考文献」を参照し、ここでの出発点となるファイルをダウンロードしてください。
リスト 2. 新しいターゲットの入力フォーム
<div id="targetFormDiv" style="position: absolute; top: 50px;
visibility: hidden; width: 100%;">
<form id="targetForm" name="targetForm">
OK, what is it? It's <input type="text" name="newTargetDisplay"
id="newTargetDisplay" />/>
<input type="button" onclick="submit_new_target()" value="Teach me!" />
</form>
</div>
|
ユーザーがアプリケーションに対して、答えが誤っていたことを伝えると、このフォームが表示され、get_new_target() 関数が呼び出されます (リスト 3)。
リスト 3. 新しいターゲットを取得する
function get_new_target(){
document.getElementById("guessDiv").style.visibility = "hidden" ;
document.getElementById("targetFormDiv").style.visibility = "visible" ;
}
|
この結果、図 1 に示すような新しいターゲットの入力フォームが表示されます (図 1)。
図 1. 新しいターゲットの入力フォーム
新しいアイテム (例えば「a lion (ライオン)」) をこのボックスに入力し、そして Teach me! をクリックすると、アプリケーションは submit_new_target() 関数を実行します (リスト 4)。
リスト 4. 新しいターゲットを送信する
...
show_form("targetFormDiv");
}
var newTarget;
function submit_new_target(){
newTarget = document.getElementById("targetForm").elements[0].value;
document.getElementById("newTarget").innerHTML = newTarget;
document.getElementById("oldTarget").innerHTML = currentGuessText;
hide_form("targetFormDiv");
show_form("answerFormDiv");
}
function hide_form(divName){
...
|
フォームから新しいターゲットを取得した後でそのターゲットを使用できるように、newTarget を宣言していることに注意してください。ターゲットを取得したら、そのターゲットの情報を answerFormDiv に設定し (リスト 5)、それを表示します。
リスト 5. answerFormDiv
<div id="answerFormDiv" style="position: absolute; top: 50px;
visibility: hidden; width: 100%;">
<form id="answerForm" name="answerForm" >
What question distinguishes <span id="newTarget"></span>
from <span id="oldTarget"></span>?<br />
<input type="text" name="newQuestion" value="" id="newQuestion" /><br />
What is the correct answer for this item?
<select id="newAnswer" name="newAnswer">
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<input type="button" value="Add Question" onclick="add_new_question()" />
</form>
</div>
|
その結果は図 2 のようになります。
図 2. 新しい質問を取得する
今度はこのデータをナレッジ・ベースに追加する必要があります。
ナレッジ・ベースにデータを追加し、また JavaScript 変数を使用する
データベースに新しいアイテムを追加するための最初のステップは、新しい question 要素を作成し、それを追加することです (リスト 6)。
リスト 6. 新しい質問を追加する
...
var nextQuestionId = 3;
var nextTargetId = 5;
function add_new_question(){
var newQuestion = document.getElementById("answerForm").elements[0].value;
var newAnswer = document.getElementById("answerForm").elements[1].value;
thisQuestionId = nextQuestionId;
nextQuestionId++;
var newQuestionXML = <question id={thisQuestionId}>
<display>{newQuestion}</display>
<answerOption>Yes</answerOption>
<answerOption>No</answerOption>
</question>;
var newQuestionElement = new XML(newQuestionXML);
knowledgeBase.questions.appendChild(newQuestionElement);
}
|
新しい質問と答えを取得したら、新しい要素を作成する必要があります。新しい質問の ID は変数 nextQuestionId から取得することができます。ID を取得したら、この変数を更新する必要があります。
次に、新しい question 要素を作成します。第 1 回で使用したものと同じ XML 構文 (「参考文献」を参照) を使いますが、ここでは JavaScript 変数の構文の中に新しい ID と表示値を挿入します。ID 属性が引用符で囲まれていないことに注目してください。{thisQuestionId} が値を表しているということが重要です。もし引用符で囲ってしまうと、「3」ではなく「{thisQuestionId}」という値になってしまいます。
XML を作成したら、その XML から XML() オブジェクトを作成し、そのオブジェクトを appendChild() メソッドを使って questions 要素に追加します。
次に、新しい答えを元のターゲットに追加します。言い換えると、もしアプリケーションが「a house cat (飼い猫)」と答え、正解は「a lion (ライオン)」だったとすると、ナレッジ・ベースに対して、「a house cat (飼い猫)」の場合は「Is it wild? (それは野生ですか?)」に対する答えは「No (イイエ)」だと教える必要があります。
リスト 7. 新しいターゲットを追加する
...
var newQuestionElement = new XML(newQuestionXML);
knowledgeBase.questions.appendChild(newQuestionElement);
//Add new answer to old target
var oldAnswer = "Yes";
if (newAnswer == "Yes"){
oldAnswer = "No";
}
var oldTargetNewAnswer = <answer questionid={thisQuestionId}></answer>;
oldTargetNewAnswer["answerValue"+thisQuestionId] = oldAnswer;
knowledgeBase..target.(@id==currentGuessId).appendChild(oldTargetNewAnswer);
...
|
ユーザーが送信した内容に基づいて古い答えと新しい答えを取得することから始めます。答えを取得したら、新しい answer 要素を作成します。そのために、ここでは次のような 2、3 のテクニックが使われています。
第 1 に、XML に変数を挿入しています (これは先ほど見ました)。
第 2 に、ハッシュ記法を使っています。oldTargetNewAnswer.answerValue3 要素を暗黙的に作成していることに注意してください。もしこの要素が存在しない場合には E4X が作成します。
最後に、ナレッジ・ベースの中にあるすべての target 要素を選択し、currentGuessId に対応するターゲットのみとなるようにフィルタリングし、そしてそのターゲットに新しい answer 要素を追加します。これによって既存のターゲットが処理されます。
今度は新しいターゲットを作成する必要があります。この新しい target 要素は、そのターゲットに適用されるそれまでの答えをすべて含む必要があります。そのため、最も簡単な方法は古いターゲットをコピーする方法です (リスト 8)。
リスト 8. 新しいターゲットを作成する
...
knowledgeBase..target.(@id==currentGuessId).appendChild(oldTargetNewAnswer);
var oldTargetElement = new XML();
oldTargetElement = knowledgeBase..target.(@id==currentGuessId);
//Clone old target
var newTargetElement = oldTargetElement.copy();
newTargetElement.@id = nextTargetId;
nextTargetId++;
newTargetElement.display = newTarget;
var newAnswerElement = newTargetElement.answer.(@questionid == thisQuestionId);
newAnswerElement["answerValue"+thisQuestionId] = newAnswer;
knowledgeBase.targets.appendChild(newTargetElement);
hide_form("answerFormDiv");
start_over();
}
|
まず、以前と同じく、古い target 要素に対する参照を取得します。次に copy() メソッドを使ってこの要素と同じものを作成します。それが作成できたら、ID と表示を新しい値にリセットする必要があります。最後に、最も新しい answer 要素の値を新しい値に変更する必要があります。
これで新しい要素をナレッジ・ベースに追加することができます。
この時点で、最初の質問を使って start_over() コマンドを実行することができ、また alert() 文を削除していない限り、「Animal」を選択した後で、残っている新しいターゲットと、(ターゲットに含まれる) 新しい質問に対する答えを確認することができます (図 3)。
図 3. 新しい値
つまりページをリロードするまでは、追加された新しいアイテムと質問は有効です (新しいアイテムと質問を見るためには、単純に新しいアイテムについて考え、それに対応する質問に答えます)。
残念なことに、ページをリロードした途端、ナレッジ・ベースは元の状態に戻ってしまいます。また、新しいアイテムを見られるのは、そのアイテムを追加したユーザーのみです。誰が新しいアイテムを追加しても、他の誰もがそれを見られたら素晴らしいと思いませんか。
新しい、改善されたアルゴリズム
誰が追加した新しい情報であっても、このゲームで遊ぶ他の誰もがその情報を利用できるようにするためには、外部データベースをセットアップする必要があります。この例では MySQL データベースと PHP を使いますが、実際の実装に関する詳細の説明はこの記事の目的からは外れるので、ここでは省略します (「ダウンロード」セクションで、ナレッジ・ベースの管理と生成を行うための、SQL ファイルと PHP スクリプトをダウンロードすることができます)。オリジナルのアルゴリズムをベースに、このデータベースとの統合を追加すると、アプリケーションのアルゴリズムは次のようなものになります。
- データベースから最も新しいナレッジ・ベースを取得します。
- そのアイテムが、「animal (動物)」なのか、「vegetable (野菜)」なのか、それとも「mineral (鉱物)」なのかをユーザーに質問します。
- 質問に対する答えに合わないアイテムをすべて排除します。
- 1 つのアイテムのみになったら、それが正しい答えかどうかをユーザーに質問します。
答えが正しければ、データベースからナレッジ・ベースをリロードして新しいデータを取得し、始めからまた繰り返します。
答えが正しくなければ、正しい答えを知るために、どんな質問をすれば正しい答えと誤った答えとを区別できたのかをユーザーに尋ねます。
- その質問をデータベースに送信し、その新しい質問用の ID を取得します。
- 新しい questionid を使って古いターゲットと新しいターゲットに答えを追加し、新しいターゲットをナレッジ・ベースに追加します。
- まだ複数のアイテムが残っているうちは、残っているアイテムのうちの最も多くのアイテムに共通する質問となるように質問を決めます。(これが重要な点です。これによって、答えとなりうるアイテムを最も多く排除することができます。)
- その質問をします。
- ステップ 3 に戻ります。
アプリケーションとデータベースとの対話動作の鍵となるのは Prototype JavaScript ライブラリーです。
Prototype の紹介
Prototype JavaScript ライブラリーではよく知られている Ajax の機能を数多く利用しますが、実はこのライブラリーは必要な機能がすべて備わった機能スイートなのです。Prototype の機能には、次の 4 つの基本機能があります。
- クラス管理: Prototype にはクラスとオブジェクトの作成や継承を容易に行える機能が含まれています。
- DOM 管理: Prototype にはページ要素 (特にフォーム) を容易にフックできる機能と、要素を表示したり非表示にしたりといったタスク用のコンビニエンス・メソッドが含まれています。
- JSON: Prototype には、ストリングから直接オブジェクトを生成する機能をはじめとし、JavaScript Object Notation との間で高速かつ安全に変換を行うためのメソッドが含まれています。
- Ajax: Prototype の中心的な機能である Ajax 機能を利用すると、外部 URL のデータを要求し、そのデータをページ上に表示することが容易になります。また Prototype には周期的な更新機能も含まれていますが、その機能はこの記事では使いません。
Prototype のクラスとメソッドを利用できるようにするためには、Prototypejs.org の Web サイトから最新のファイルをダウンロードし、それを HTML のページに追加します (リスト 9)。
リスト 9. HTML のページに Prototype を追加する
<html>
<head>
<title>E4X mindreader</title>
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript; e4x=1" src="e4x.js"></script>
...
|
では、いくつかの簡単なタスクから始めることにしましょう。
フォームの管理
生の DOM を使って Web ページのコンテンツを操作することは確かに可能ですが、Prototype はいくつかのコンビニエンス関数を持っています。例えば、$() function 関数を使って要素にアクセスすることができます。つまり式 $('answerFormDiv') は answerFormDiv という ID を持つ要素を参照します。この機能を利用すると、第 1 回の中にあった大量の DOM アクションを整理することができます。例えば、document.getElementById("displayQuestion").innerHTML = questionDisplay ; を $("displayQuestion").innerHTML = questionDisplay ; で置き換えることができます。
また $F() 関数を使うとフォーム要素へのアクセスも容易になります。例えば、newTarget = document.getElementById("targetForm").elements[0].value; を newTarget = $F("newTargetDisplay"); で置き換えることができます。
これらの関数は HTML 要素の ID 属性を使います。そのため、それらの属性を必ず HTML ファイルの中に含める必要があります。
今度は、このアプリケーションをバックエンドのデータベースと統合するためには何が必要かを調べましょう。
ナレッジ・ベースを要求する
ナレッジ・ベースを要求するためには Prototype の Ajax.Request オブジェクトを使います。このオブジェクトを使って HTTP リクエストを送信し、その結果に基づいて新しい関数を呼び出します。この場合はナレッジ・ベースを表現する XML ストリングを要求し、そのストリングを使って XML オブジェクトを作成します (リスト 10)。
リスト 10. e4x.js でナレッジ・ベースを要求する
function get_knowledge_base(){
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {getkb: 'YES'},
onSuccess: function(transport){
kstring = transport.responseText;
knowledgeBase = new XML(kstring);
start_over();
}
})
}
|
新しい Ajax.Request オブジェクトを作成したら、このオブジェクトに大量のデータを渡します。まず、必要な HTTP リクエストの URL をこのオブジェクトに渡します。ホスト名を指定することもできますが、Ajax リクエストは通常、リクエストを行っているページと同じサーバーとポートから発行される必要があります。そのため、ここでは単純に相対 URL を使います。
また、GET メソッドを使用すること、YES という値を持つ 1 つのパラメーター getkb を持っていることも指定します。parameters パラメーターを使って複数の値を設定することもできます (これについてはこのすぐ後に説明します)。
この関数でおそらく最も重要な部分は onSuccess ハンドラーでしょう。このハンドラーは Ajax.Request オブジェクトに対して、リクエストが成功した場合に何をする必要があるかを指示します。(Prototype は多種多様なハンドラーを 7 種類も定義しています。そのため、ほとんどすべてのことに対応することができます。) リクエストが成功すると、このスクリプトはそのリクエストのテキストを抽出し (このテキストが文書を表現する単なる XML であることを思い出してください)、そのテキストを使って knowledgeBase を新しい XML() オブジェクトとして作成します。それが終わると、start_over() 関数を実行することでブラウザーによるページの描画が始まります。
このファイルを保存してページをリロードすると、すべてのものが以前と同じように動作することがわかりますが、2 点例外があります。第 1 に、もしアラートが相変わらずデータを表示している場合には、データベースがナレッジ・ベースのデータを現在処理しているということです。第 2 に、もし統合されたデータベースを他の人達が更新したとすると、既にプログラム済みのターゲットが以前よりも増えています。
新しいデータの話題が出たところで、新しいデータを入力する方法を調べてみましょう。
新しい質問を送信する
Prototype で Ajax 関数を扱う際に認識しておく必要のある 1 つの重要な点が、Ajax 関数は必然的に非同期であるということです。ユーザーにできることは、リクエストを送信し、それに対するレスポンスが返ってきたときに何をするのかを記しておくことだけです。そうです。リクエストを送信してそのまま待つ、ということはできないため、代わりにレスポンスが返ってきたときに何をするべきなのかを指定する必要があるのです。したがって、add_new_question() 関数を分割する必要があります。まず質問を送信しますが、questionid を取得するまではターゲットを追加することはできないからです (リスト 11)。
リスト 11. 質問を送信する
function add_new_question(){
var newQuestion = $F('newQuestion');
var newAnswer = $F('newAnswer');
var thisQuestionId;
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {getkb: 'NO', question: newQuestion},
onSuccess: function(transport){
thisQuestionId = transport.responseText;
finish_adding_new_question(newQuestion, newAnswer, thisQuestionId);
}
})
}
|
これは非常に単純です。最初に新しい質問と答えを取得し、それを PHP ファイルに送信します。パラメーターの中で、カンマで区切られた名前と値のペアが複数使われていることに注目してください。
ここまで実行した時点で、この関数の処理は終了し、スクリプトに処理が返るまで待つことになります。スクリプトに処理が返ると、新しい質問の ID が取得できているので、このルーチンの後半に進むことができます (リスト 12)。
リスト 12. 新しい質問の追加を完了する
function finish_adding_new_question(newQuestion, newAnswer, thisQuestionId){
var newQuestionXML = <question id={thisQuestionId}>
<display>{newQuestion}</display>
<answerOption>Yes</answerOption>
<answerOption>No</answerOption>
</question>;
var oldAnswer = "Yes";
if (newAnswer == "Yes"){
oldAnswer = "No";
}
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {addAnswers: 'YES',
old_target_id: currentGuessId,
old_answer: oldAnswer,
target_display: newTarget,
question_id: thisQuestionId,
new_answer: newAnswer},
onSuccess: function(transport){
get_knowledge_base();
}
})
}
|
これまでと同じく、question 要素の内部を作成し、古い答えと新しい答えを決める必要があります。それができたら新しいリクエストを開始することができます。この新しいリクエストには複数のパラメーターが含まれていることに注意してください。このリクエストは、これらのパラメーターの情報をすべて PHP スクリプトに送信します。PHP スクリプトはこの情報をすべて取得し、データベースの中で新しいターゲットを作成します。
このルーチンに処理が返ってきたら、ナレッジ・ベースを要求すれば最新の情報を取得することができます。
まとめと次のステップ
ここで作成したものは、とても人工知能と言えるようなものではありませんが、それでもユーザーを楽しませるだけの十分な機能を提供しています。ユーザーがこのゲームで遊び、ナレッジ・ベースの中にないアイテムを考えるたびに、このアプリケーションはそのアイテムと、そのアイテムに対する適切な質問と答えをデータベースに追加します。Prototype JavaScript ライブラリーを使うことで、アプリケーションはナレッジ・ベースの新しいコピーをアプリケーションに要求できるようになり、ユーザーがゲームで遊ぶたびに常に最新の情報を得られるようになります。
もちろん、このソリューション自体には問題があります。この記事の執筆時点ではナレッジ・ベースは極めて小規模です。もし多くの人がこのナレッジ・ベースを使って遊び、大量のアイテムを追加したとすると、データベース全体をダウンロードすることは、たった 1 度ですら現実的ではないもしれません。ましてやユーザーが新しいゲームを始めるたびにダウンロードすることなど、もっと非現実的です。
そうなった場合には、いくつかの選択肢があります。最も単純な方法は、関係のあるセットのみを送信する方法です。例えば、最初の「animal (動物)」なのか、「vegetable (野菜)」なのか、それとも「mineral (鉱物)」なのかという質問をハードコーディングし、ナレッジ・ベースのうち、その質問の答えとなりうる 3 分の 1 のみをダウンロードします。最終的にデータベースが非常に大きくなったら、おそらくブラウザーではなくサーバーで処理をしたいと思うようになるでしょう。しかしそれでは、この記事が意図した内容からは大きく離れてしまいます。この記事では小規模な読心術アプリケーションを E4X と Prototype を使って作成する方法を説明しました。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Part 2 sample code | x-e4xpart2code.zip | 5KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
-
Prototype ライブラリーをダウンロードしてください。そして動的な Web アプリケーションをクラス駆動型で開発するための JavaScript フレームワークであり、使いやすいツールキットでもある、この Ajax ライブラリーを試してください。
- 皆さんの次期開発プロジェクトを IBM trial software で構築してください。developerWorks から直接ダウンロードすることができます。
議論するために
著者について  | |  | Nicholas Chase は、Lucent Technologies や Sun Microsystems、Oracle、the Tampa Bay Buccaneers などの会社で Web サイト開発に携わってきました。彼は高校の物理の先生であり、低レベル放射性廃棄物施設の管理者であり、オンライン SF 雑誌の編集者であり、マルチメディアのエンジニアであり、Oracle インストラクターであり、対話型コミュニケーションを提供する会社の最高技術責任者でもあります。また『XML Primer Plus』(Sams 刊) を始めとする何冊かの著書があります。彼は Second Life のコンテンツとアプリケーションの作成を専門とする InterSection Unlimited のパートナーでもあります。in-world では Chase Marellan という名前を使っています。 |
記事の評価
|