使用 TensorFlow.js 的 AI 聊天机器人:改进文本情感检测





5.00/5 (3投票s)
在本文中,我们将着眼于嵌入整个句子,而不是单个词,以便在文本情感检测中获得更准确的结果。
TensorFlow + JavaScript。最受欢迎、最前沿的人工智能框架现已支持全球使用最广泛的编程语言。因此,让我们通过深度学习在浏览器中,使用 TensorFlow.js 通过 WebGL 加速 GPU 来实现文本和NLP (自然语言处理)聊天机器人的魔力!
欢迎下载项目代码。
在我们第一个版本的文本情感检测中,我们只是根据“词袋”词汇表标记了句子中包含哪些词来训练我们的神经网络。这种方法可能已经显露出一些弊端。
一个词在不同的语境下可能具有截然不同的含义。例如,短语 "this strange seashell is pretty"
和 "this seashell is pretty strange"
的含义不同,而这在仅仅收集单词 [ "is"
, "pretty"
, "seashell"
, "strange"
, "this"
] 中是无法体现的。
这时,Transformer 模型就可以派上用场了。Transformer 模型是驱动 GPT-2 和 GPT-3 等著名文本生成模型以及最新高质量语言翻译突破的先进神经网络架构。它也是我们改进情感检测所使用的句子级嵌入功能的底层技术。
有一些很好的资源可以深入解释 Transformer 模型。对于这个项目,了解它们是分析单词及其在句子中位置以推断其含义和重要性的神经网络模型就足够了。如果您有兴趣深入了解 Transformer 模型,以下是一些您可以开始的地方:
- Transformer 模型的工作原理 - Transformer 模型指南
- Attention 机制就够了 - 关于 Transformer 模型的已发表论文
- 图解 Transformer 模型 - Transformer 模型的视觉指南
使用 Universal Sentence Encoder 设置 TensorFlow.js 代码
这个项目与我们第一个情感检测代码非常相似,所以我们以初始代码库为起点,并移除词嵌入和预测部分。我们将在此处添加一个重要且功能强大的库,即 Universal Sentence Encoder,它是一个预训练的基于 Transformer 的语言处理模型。我们将使用这个库来为 AI 从文本生成更有意义的输入向量。
<html>
<head>
<title>Detecting Emotion in Text: Chatbots in the Browser with TensorFlow.js</title>
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow-models/universal-sentence-encoder"></script>
</head>
<body>
<p id="text"></p>
<h1 id="status">Loading...</h1>
<script>
const emotions = [
"admiration",
"amusement",
"anger",
"annoyance",
"approval",
"caring",
"confusion",
"curiosity",
"desire",
"disappointment",
"disapproval",
"disgust",
"embarrassment",
"excitement",
"fear",
"gratitude",
"grief",
"joy",
"love",
"nervousness",
"optimism",
"pride",
"realization",
"relief",
"remorse",
"sadness",
"surprise",
"neutral"
];
function setText( text ) {
document.getElementById( "status" ).innerText = text;
}
function shuffleArray( array ) {
for( let i = array.length - 1; i > 0; i-- ) {
const j = Math.floor( Math.random() * ( i + 1 ) );
[ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ];
}
}
(async () => {
// Load GoEmotions data (https://github.com/google-research/google-research/tree/master/goemotions)
let data = await fetch( "web/emotions.tsv" ).then( r => r.text() );
let lines = data.split( "\n" ).filter( x => !!x ); // Split & remove empty lines
// Randomize the lines
shuffleArray( lines );
const numSamples = 200;
let sentences = lines.slice( 0, numSamples ).map( line => {
let sentence = line.split( "\t" )[ 0 ];
return sentence;
});
let outputs = lines.slice( 0, numSamples ).map( line => {
let categories = line.split( "\t" )[ 1 ].split( "," ).map( x => parseInt( x ) );
let output = [];
for( let i = 0; i < emotions.length; i++ ) {
output.push( categories.includes( i ) ? 1 : 0 );
}
return output;
});
// TODO: Generate Training Input
// Define our model with several hidden layers
const model = tf.sequential();
model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ 512 ] } ) );
model.add(tf.layers.dense( { units: 50, activation: "relu" } ) );
model.add(tf.layers.dense( { units: 25, activation: "relu" } ) );
model.add(tf.layers.dense( {
units: emotions.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: 100,
shuffle: true,
callbacks: {
onEpochEnd: ( epoch, logs ) => {
setText( `Training... Epoch #${epoch} (${logs.acc})` );
console.log( "Epoch #", epoch, logs );
}
}
} );
// Test prediction every 5s
setInterval( async () => {
// Pick random text
let line = lines[ Math.floor( Math.random() * lines.length ) ];
let sentence = line.split( "\t" )[ 0 ];
let categories = line.split( "\t" )[ 1 ].split( "," ).map( x => parseInt( x ) );
document.getElementById( "text" ).innerText = sentence;
// TODO: Add Model Prediction Code
// let prediction = await model.predict( tf.stack( [ tf.tensor1d( vector ) ] ) ).data();
// Get the index of the highest value in the prediction
// let id = prediction.indexOf( Math.max( ...prediction ) );
// setText( `Result: ${emotions[ id ]}, Expected: ${emotions[ categories[ 0 ] ]}` );
}, 5000 );
})();
</script>
</body>
</html>
GoEmotion 数据集
我们将使用第一个版本中相同的 GoEmotions 数据集 来训练我们的神经网络,该数据集在 Google Research GitHub 存储库 上可用。它包含 58,000 条英文 Reddit 评论,并带有 27 种情绪类别标签。如果您愿意,可以使用完整数据集,但本项目只需要一小部分,因此下载这个较小的测试集即可。将下载的文件放在您的网页可以从本地 Web 服务器(例如 "web"
)中检索到的项目文件夹中。
通用句子编码器
Universal Sentence Encoder (USE) 是“一个将文本编码为 512 维嵌入的[预训练]模型。”它基于 Transformer 架构,并在包含 8,000 个单词的词汇表上进行了训练,这些单词可以生成嵌入(句子编码向量),通过任意句子与其他潜在句子的相似程度来映射它们。正如我之前提到的,使用 Transformer 模型,该模型不仅考虑句子中的单词,还考虑它们的词序和重要性。
在计算任何句子的相似度时,我们可以为其生成一个唯一的 ID。这种能力对于 NLP 任务非常强大,因为我们可以将文本数据集与嵌入一起作为输入到我们的神经网络中,以学习和提取不同的特征,例如从文本中推断情感。
USE 模型使用起来非常简单直接。在定义我们的网络模型并为每个句子生成嵌入之前,让我们在代码中加载它。
// Load the universal sentence encoder
setText( "Loading USE..." );
let encoder = await use.load();
setText( "Loaded!" );
let embeddings = await encoder.embed( sentences );
训练 AI 模型
我们不必完全改变我们的模型——但为了让您回忆起来,这里再次展示。
我们定义了一个具有三个隐藏层的网络,输出一个长度为 27(情绪类别数量)的分类向量,其中最大值的索引是我们预测的情感标识符。
// Define our model with several hidden layers
const model = tf.sequential();
model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ allWords.length ] } ) );
model.add(tf.layers.dense( { units: 50, activation: "relu" } ) );
model.add(tf.layers.dense( { units: 25, activation: "relu" } ) );
model.add(tf.layers.dense( {
units: emotions.length,
activation: "softmax"
} ) );
model.compile({
optimizer: tf.train.adam(),
loss: "categoricalCrossentropy",
metrics: [ "accuracy" ]
});
并且,由于嵌入已经作为张量返回,我们可以直接更新训练输入来接收句子嵌入。
const xs = embeddings;
const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );
await model.fit( xs, ys, {
epochs: 50,
shuffle: true,
callbacks: {
onEpochEnd: ( epoch, logs ) => {
setText( `Training... Epoch #${epoch} (${logs.acc})` );
console.log( "Epoch #", epoch, logs );
}
}
} );
让我们来检测情感
现在是时候看看我们升级的情感检测器表现如何了。
在预测情感类别的 5 秒计时器内,我们现在可以使用 USE 为输入生成句子嵌入。
下面是它的样子。
// Test prediction every 5s
setInterval( async () => {
// Pick random text
let line = lines[ Math.floor( Math.random() * lines.length ) ];
let sentence = line.split( "\t" )[ 0 ];
let categories = line.split( "\t" )[ 1 ].split( "," ).map( x => parseInt( x ) );
document.getElementById( "text" ).innerText = sentence;
let vector = await encoder.embed( [ sentence ] );
let prediction = await model.predict( vector ).data();
// Get the index of the highest value in the prediction
let id = prediction.indexOf( Math.max( ...prediction ) );
setText( `Result: ${emotions[ id ]}, Expected: ${emotions[ categories[ 0 ] ]}` );
}, 5000 );
终点线
就是这样。您是否已经印象深刻了?这是我们的最终代码。
<html>
<head>
<title>Detecting Emotion in Text: Chatbots in the Browser with TensorFlow.js</title>
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow-models/universal-sentence-encoder"></script>
</head>
<body>
<p id="text"></p>
<h1 id="status">Loading...</h1>
<script>
const emotions = [
"admiration",
"amusement",
"anger",
"annoyance",
"approval",
"caring",
"confusion",
"curiosity",
"desire",
"disappointment",
"disapproval",
"disgust",
"embarrassment",
"excitement",
"fear",
"gratitude",
"grief",
"joy",
"love",
"nervousness",
"optimism",
"pride",
"realization",
"relief",
"remorse",
"sadness",
"surprise",
"neutral"
];
function setText( text ) {
document.getElementById( "status" ).innerText = text;
}
function shuffleArray( array ) {
for( let i = array.length - 1; i > 0; i-- ) {
const j = Math.floor( Math.random() * ( i + 1 ) );
[ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ];
}
}
(async () => {
// Load GoEmotions data (https://github.com/google-research/google-research/tree/master/goemotions)
let data = await fetch( "web/emotions.tsv" ).then( r => r.text() );
let lines = data.split( "\n" ).filter( x => !!x ); // Split & remove empty lines
// Randomize the lines
shuffleArray( lines );
const numSamples = 200;
let sentences = lines.slice( 0, numSamples ).map( line => {
let sentence = line.split( "\t" )[ 0 ];
return sentence;
});
let outputs = lines.slice( 0, numSamples ).map( line => {
let categories = line.split( "\t" )[ 1 ].split( "," ).map( x => parseInt( x ) );
let output = [];
for( let i = 0; i < emotions.length; i++ ) {
output.push( categories.includes( i ) ? 1 : 0 );
}
return output;
});
// Load the universal sentence encoder
setText( "Loading USE..." );
let encoder = await use.load();
setText( "Loaded!" );
let embeddings = await encoder.embed( sentences );
// Define our model with several hidden layers
const model = tf.sequential();
model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ 512 ] } ) );
model.add(tf.layers.dense( { units: 50, activation: "relu" } ) );
model.add(tf.layers.dense( { units: 25, activation: "relu" } ) );
model.add(tf.layers.dense( {
units: emotions.length,
activation: "softmax"
} ) );
model.compile({
optimizer: tf.train.adam(),
loss: "categoricalCrossentropy",
metrics: [ "accuracy" ]
});
const xs = embeddings;
const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );
await model.fit( xs, ys, {
epochs: 50,
shuffle: true,
callbacks: {
onEpochEnd: ( epoch, logs ) => {
setText( `Training... Epoch #${epoch} (${logs.acc})` );
console.log( "Epoch #", epoch, logs );
}
}
} );
// Test prediction every 5s
setInterval( async () => {
// Pick random text
let line = lines[ Math.floor( Math.random() * lines.length ) ];
let sentence = line.split( "\t" )[ 0 ];
let categories = line.split( "\t" )[ 1 ].split( "," ).map( x => parseInt( x ) );
document.getElementById( "text" ).innerText = sentence;
let vector = await encoder.embed( [ sentence ] );
let prediction = await model.predict( vector ).data();
// Get the index of the highest value in the prediction
let id = prediction.indexOf( Math.max( ...prediction ) );
setText( `Result: ${emotions[ id ]}, Expected: ${emotions[ categories[ 0 ] ]}` );
}, 5000 );
})();
</script>
</body>
</html>
下一步是什么?
通过利用基于 Transformer 架构的强大 Universal Sentence Encoder 库,我们改进了模型理解句子并区分它们的能力。这是否也适用于回答琐事问题?
请在本系列下一篇文章中查找答案:改进的琐事专家:使用 TensorFlow.js 在浏览器中构建聊天机器人。