目录
设置TensorFlow.js代码
TriviaQA数据集
单词嵌入和标记
训练AI模型
聊天机器人(Trivia Chatbot)在行动
终点线
下一步是什么?
- 下载项目文件-9.9 MB
TensorFlow + JavaScript。现在,最流行,最先进的AI框架支持地球上使用最广泛的编程语言。因此,让我们在Web浏览器中通过深度学习使文本和NLP(自然语言处理)聊天机器人神奇地发生,使用TensorFlow.js通过WebGL加速GPU!
在上一篇文章中,我们带您完成了一个AI模型的训练过程,该模型可以使用TensorFlow在浏览器中为任何英语句子计算27种情绪之一。在这一部分中,我们将构建一个聊天机器人。
很好地回答聊天问题需要知道无数事实,并且能够准确地回忆起相关知识。利用计算机的大脑真是一个绝佳的机会!
让我们训练一个聊天机器人,使用递归神经网络(RNN)为我们提供数百个不同聊天问题的答案。
设置TensorFlow.js代码在此项目中,我们将与聊天机器人进行交互,因此让我们将一些输入元素和文本响应从该机器人添加到我们的模板网页中。
Trivia Know-It-All: Chatbots in the Browser with TensorFlow.js
Trivia Know-It-All Bot
Ask a trivia question:
Submit
function setText( text ) {
document.getElementById( "status" ).innerText = text;
}
(async () => {
// Your Code Goes Here
})();
TriviaQA数据集
我们将用于训练神经网络的数据来自华盛顿大学提供的TriviaQA数据集。2.5万个压缩文件中有9.5万个聊天问答对,可供下载。
现在,我们将使用一个较小的子集verified-wikipedia-dev.json,该子集包含在该项目的示例代码中。
TriviaQA JSON文件由一个Data数组组成,该数组具有各个Q&A元素,这些元素看起来类似于以下示例文件:
{
"Data": [
{
"Answer": {
"Aliases": [
"Sunset Blvd",
"West Sunset Boulevard",
"Sunset Boulevard",
"Sunset Bulevard",
"Sunset Blvd."
],
"MatchedWikiEntityName": "Sunset Boulevard",
"NormalizedAliases": [
"west sunset boulevard",
"sunset blvd",
"sunset boulevard",
"sunset bulevard"
],
"NormalizedMatchedWikiEntityName": "sunset boulevard",
"NormalizedValue": "sunset boulevard",
"Type": "WikipediaEntity",
"Value": "Sunset Boulevard"
},
"EntityPages": [
{
"DocSource": "TagMe",
"Filename": "Andrew_Lloyd_Webber.txt",
"LinkProbability": "0.02934",
"Rho": "0.22520",
"Title": "Andrew Lloyd Webber"
}
],
"Question": "Which Lloyd Webber musical premiered in the US on 10th December 1993?",
"QuestionId": "tc_33",
"QuestionSource": "http://www.triviacountry.com/",
"SearchResults": [
{
"Description": "The official website for Andrew Lloyd Webber, ... from the Andrew Lloyd Webber/Jim Steinman musical Whistle ... American premiere on 9th December 1993 at the ...",
"DisplayUrl": "www.andrewlloydwebber.com",
"Filename": "35/35_995.txt",
"Rank": 0,
"Title": "Andrew Lloyd Webber | The official website for Andrew ...",
"Url": "http://www.andrewlloydwebber.com/"
}
]
}
],
"Domain": "Web",
"VerifiedEval": false,
"Version": 1.0
}
我们可以像这样在我们的代码中加载数据:
(async () => {
// Load TriviaQA data
let triviaData = await fetch( "web/verified-wikipedia-dev.json" ).then( r => r.json() );
let data = triviaData.Data;
// Process all QA to map to answers
let questions = data.map( qa => qa.Question );
})();
单词嵌入和标记
对于这些聊天问题以及一般的英语句子、单词的位置和顺序可能会影响其含义。因此,当将句子变成向量时,我们不能简单地使用不保留单词位置信息的“单词袋”。因此,在准备训练数据时,我们将使用一种称为word embedding的方法,并创建一个表示单词及其位置的单词索引列表。
首先,我们将遍历所有可用数据,并在所有问题中识别每个唯一的单词,就像准备一袋单词时一样。我们想在wordReference索引中添加+1以保留索引0作为TensorFlow中的填充令牌。
let bagOfWords = {};
let allWords = [];
let wordReference = {};
questions.forEach( q => {
let words = q.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
words.forEach( w => {
if( !bagOfWords[ w ] ) {
bagOfWords[ w ] = 0;
}
bagOfWords[ w ]++; // Counting occurrence just for word frequency fun
});
});
allWords = Object.keys( bagOfWords );
allWords.forEach( ( w, i ) => {
wordReference[ w ] = i + 1;
});
在拥有包含所有单词及其索引的完整词汇表之后,我们可以采用每个疑问句并创建与每个单词的索引相对应的正整数数组。我们需要确保输入向量(进入网络)的长度相同。我们可以将句子的最大数量限制为30个单词,并且任何少于30个单词的问题都可以设置零索引来表示空白填充。
让我们还生成预期的输出分类向量,这些向量映射到每个不同的问答对。
// Create a tokenized vector for each question
const maxSentenceLength = 30;
let vectors = [];
questions.forEach( q => {
let qVec = [];
// Use a regex to only get spaces and letters and remove any blank elements
let words = q.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
for( let i = 0; i < maxSentenceLength; i++ ) {
if( words[ i ] ) {
qVec.push( wordReference[ words[ i ] ] );
}
else {
// Add padding to keep the vectors the same length
qVec.push( 0 );
}
}
vectors.push( qVec );
});
let outputs = questions.map( ( q, index ) => {
let output = [];
for( let i = 0; i < questions.length; i++ ) {
output.push( i === index ? 1 : 0 );
}
return output;
});
训练AI模型
TensorFlow为像我们刚刚创建的标记化矢量提供了一种嵌入层类型,并将其转化为可用于神经网络的密集矢量。我们使用RNN架构是因为单词的顺序在每个问题中都很重要。我们可以使用简单的RNN层或双向的神经网络来训练神经网络。随意取消注释/注释代码行,并尝试其中之一。
网络应返回一个分类向量,其中最大值的索引将与问题答案对的索引对应。模型的完成设置应如下所示:
// Define our RNN model with several hidden layers
const model = tf.sequential();
// Add 1 to inputDim for the "padding" character
model.add(tf.layers.embedding( { inputDim: allWords.length + 1, outputDim: 128, inputLength: maxSentenceLength } ) );
// model.add(tf.layers.simpleRNN( { units: 32 } ) );
model.add(tf.layers.bidirectional( { layer: tf.layers.simpleRNN( { units: 32 } ), mergeMode: "concat" } ) );
model.add(tf.layers.dense( { units: 50 } ) );
model.add(tf.layers.dense( { units: 25 } ) );
model.add(tf.layers.dense( {
units: questions.length,
activation: "softmax"
} ) );
model.compile({
optimizer: tf.train.adam(),
loss: "categoricalCrossentropy",
metrics: [ "accuracy" ]
});
最后,我们可以将输入数据转换为张量并训练网络。
const xs = tf.stack( vectors.map( x => tf.tensor1d( x ) ) );
const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );
await model.fit( xs, ys, {
epochs: 20,
shuffle: true,
callbacks: {
onEpochEnd: ( epoch, logs ) => {
setText( `Training... Epoch #${epoch} (${logs.acc})` );
console.log( "Epoch #", epoch, logs );
}
}
} );
聊天机器人(Trivia Chatbot)在行动
我们快准备好了。
要测试我们的聊天机器人,我们需要能够通过提交问题并使其回答做出“交谈”。让我们在机器人经过训练并准备就绪时通知用户,并处理用户输入:
setText( "Trivia Know-It-All Bot is Ready!" );
document.getElementById( "question" ).addEventListener( "keyup", function( event ) {
// Number 13 is the "Enter" key on the keyboard
if( event.keyCode === 13 ) {
// Cancel the default action, if needed
event.preventDefault();
// Trigger the button element with a click
document.getElementById( "submit" ).click();
}
});
document.getElementById( "submit" ).addEventListener( "click", async function( event ) {
let text = document.getElementById( "question" ).value;
document.getElementById( "question" ).value = "";
// Our prediction code will go here
});
最后,在我们的“click”事件处理程序中,我们可以像对待训练问题一样,标记用户提交的问题。然后,我们可以让模型发挥作用,预测最可能被问到的问题,并显示聊天问题和答案。
在测试聊天机器人时,您可能会注意到单词的顺序似乎影响太大,或者问题中的第一个单词会严重影响其输出。我们将在下一篇文章中对此进行改进。同时,您可以使用另一种方法来解决此问题,该方法称为Attention,以训练bot权衡某些单词的权重。
如果您想了解更多信息,我建议您查看这篇关于可视化的文章,其中介绍了注意在序列到序列模型中如何有用。
现在这是我们的完整代码:
Trivia Know-It-All: Chatbots in the Browser with TensorFlow.js
Trivia Know-It-All Bot
Ask a trivia question:
Submit
function setText( text ) {
document.getElementById( "status" ).innerText = text;
}
(async () => {
// Load TriviaQA data
let triviaData = await fetch( "web/verified-wikipedia-dev.json" ).then( r => r.json() );
let data = triviaData.Data;
// Process all QA to map to answers
let questions = data.map( qa => qa.Question );
let bagOfWords = {};
let allWords = [];
let wordReference = {};
questions.forEach( q => {
let words = q.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
words.forEach( w => {
if( !bagOfWords[ w ] ) {
bagOfWords[ w ] = 0;
}
bagOfWords[ w ]++; // Counting occurrence just for word frequency fun
});
});
allWords = Object.keys( bagOfWords );
allWords.forEach( ( w, i ) => {
wordReference[ w ] = i + 1;
});
// Create a tokenized vector for each question
const maxSentenceLength = 30;
let vectors = [];
questions.forEach( q => {
let qVec = [];
// Use a regex to only get spaces and letters and remove any blank elements
let words = q.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
for( let i = 0; i < maxSentenceLength; i++ ) {
if( words[ i ] ) {
qVec.push( wordReference[ words[ i ] ] );
}
else {
// Add padding to keep the vectors the same length
qVec.push( 0 );
}
}
vectors.push( qVec );
});
let outputs = questions.map( ( q, index ) => {
let output = [];
for( let i = 0; i < questions.length; i++ ) {
output.push( i === index ? 1 : 0 );
}
return output;
});
// Define our RNN model with several hidden layers
const model = tf.sequential();
// Add 1 to inputDim for the "padding" character
model.add(tf.layers.embedding( { inputDim: allWords.length + 1, outputDim: 128, inputLength: maxSentenceLength, maskZero: true } ) );
model.add(tf.layers.simpleRNN( { units: 32 } ) );
// model.add(tf.layers.bidirectional( { layer: tf.layers.simpleRNN( { units: 32 } ), mergeMode: "concat" } ) );
model.add(tf.layers.dense( { units: 50 } ) );
model.add(tf.layers.dense( { units: 25 } ) );
model.add(tf.layers.dense( {
units: questions.length,
activation: "softmax"
} ) );
model.compile({
optimizer: tf.train.adam(),
loss: "categoricalCrossentropy",
metrics: [ "accuracy" ]
});
const xs = tf.stack( vectors.map( x => tf.tensor1d( x ) ) );
const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );
await model.fit( xs, ys, {
epochs: 20,
shuffle: true,
callbacks: {
onEpochEnd: ( epoch, logs ) => {
setText( `Training... Epoch #${epoch} (${logs.acc})` );
console.log( "Epoch #", epoch, logs );
}
}
} );
setText( "Trivia Know-It-All Bot is Ready!" );
document.getElementById( "question" ).addEventListener( "keyup", function( event ) {
// Number 13 is the "Enter" key on the keyboard
if( event.keyCode === 13 ) {
// Cancel the default action, if needed
event.preventDefault();
// Trigger the button element with a click
document.getElementById( "submit" ).click();
}
});
document.getElementById( "submit" ).addEventListener( "click", async function( event ) {
let text = document.getElementById( "question" ).value;
document.getElementById( "question" ).value = "";
// Run the calculation things
let qVec = [];
let words = text.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
for( let i = 0; i < maxSentenceLength; i++ ) {
if( words[ i ] ) {
qVec.push( wordReference[ words[ i ] ] );
}
else {
// Add padding to keep the vectors the same length
qVec.push( 0 );
}
}
let prediction = await model.predict( tf.stack( [ tf.tensor1d( qVec ) ] ) ).data();
// Get the index of the highest value in the prediction
let id = prediction.indexOf( Math.max( ...prediction ) );
document.getElementById( "bot-question" ).innerText = questions[ id ];
document.getElementById( "bot-answer" ).innerText = data[ id ].Answer.Value;
});
})();
下一步是什么?
使用RNN,我们创建了一个深度学习聊天机器人来识别问题,并在浏览器中直接从大量聊天问题/答案对中为我们提供答案。接下来,我们将研究嵌入整个句子而不是单个单词,以便在从文本中检测情感时获得更准确的结果。
和我一起参加本系列的下一篇文章中,使用TensorFlow.js在浏览器中改进文本情感检测。
https://www.codeproject.com/Articles/5282688/AI-Chatbots-With-TensorFlow-js-Training-a-Trivia-E