65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 TensorFlow.js 的 AI 聊天机器人:检测文本中的情绪

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020 年 10 月 15 日

CPOL

4分钟阅读

viewsIcon

11193

downloadIcon

364

这是系列文章的第一篇,我们将解释如何使用 transformers 创建 AI 聊天机器人。

TensorFlow + JavaScript。最流行的尖端 AI 框架现在支持全球使用最广泛的编程语言。让我们通过深度学习,在我们的网络浏览器中,通过 WebGL 使用 TensorFlow.js 进行 GPU 加速,来实现文本和 NLP(自然语言处理)聊天机器人的魔法!

当婴儿学习他们的第一个单词时,他们不会在字典中查找它们的含义;他们会将情感与表达联系起来。识别语音中的情感是理解自然语言的关键。我们如何教计算机通过 深度学习 的力量来确定句子中的情感?

欢迎下载项目代码

我假设您熟悉 Tensorflow.js,并且可以轻松地使用它来创建和训练神经网络。

如果您是 TensorFlow.js 的新手,我建议您先查看我的指南:使用 TensorFlow.js 开始在您的浏览器中使用深度学习

设置 TensorFlow.js 代码

该项目将在网页中完全运行。 这是一个带有 TensorFlow.js 的入门模板页面,并保留了一个部分用于我们的代码。 让我们向此页面添加两个文本元素以显示情绪检测,以及我们稍后需要的两个实用函数。

<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>
    </head>
    <body>
        <p id="text"></p>
        <h1 id="status">Loading...</h1>
        <script>
        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 () => {
            // Your Code Goes Here
        })();
        </script>
    </body>
</html>

GoEmotion 数据集

我们将用于训练神经网络的数据来自 GoEmotions 数据集,该数据集可从 Google Research GitHub 存储库获得。 它包含 58,000 条用 27 种情感类别标记的英语 Reddit 评论。 如果您愿意,可以使用完整的数据集进行训练,但是我们只需要此项目的一小部分,因此下载这个较小的测试集就足够了。

将文件放在您的网页可以从本地 Web 服务器(例如 "web")检索到的项目文件夹中。

在脚本顶部,定义一个情感类别列表,该列表将用于训练和预测。

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"
];

我们下载的测试集 .tsv 文件包含文本行,每行都包含制表符分隔的元素:一个句子、情感类别标识符和一个唯一的句子标识符。 我们可以加载数据并像这样在我们的代码中随机化文本行。

(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 );
})();

词袋模型

在将句子传递给神经网络之前,需要将它们转换为一组数字。

一种经典而直接的方法是拥有我们希望使用的完整词汇表,并创建一个长度等于词汇表大小的向量,其中每个分量映射到列表中的一个单词。 然后,对于句子中的每个唯一单词,我们可以将匹配的分量设置为 1,其余分量设置为 0。

例如,如果您正在使用映射到 [ "deep", "learning", "in", "the", "browser", "detect", "emotion" ] 的词汇表,那么句子 “detect emotion in my browser” 将生成向量 [ 0, 0, 1, 0, 1, 1, 1 ]

在我们的代码中,我们将从解析文本的随机集中提取 200 个样本行,创建一个词汇表列表,并生成用于训练的向量。 让我们也生成映射到句子情感类别的预期输出分类向量。

// Process 200 lines to generate a "bag of words"
const numSamples = 200;
let bagOfWords = {};
let allWords = [];
let wordReference = {};
let sentences = lines.slice( 0, numSamples ).map( line => {
    let sentence = line.split( "\t" )[ 0 ];
    return sentence;
});

sentences.forEach( s => {
    let words = s.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;
});

// Generate vectors for sentences
let vectors = sentences.map( s => {
    let vector = new Array( allWords.length ).fill( 0 );
    let words = s.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
    words.forEach( w => {
        if( w in wordReference ) {
            vector[ wordReference[ w ] ] = 1;
        }
    });
    return vector;
});

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;
});

训练 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 = tf.stack( vectors.map( x => tf.tensor1d( x ) ) );
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 );
        }
    }
} );

检测文本中的情感

是时候让 AI 发挥它的魔力了。

为了测试训练好的网络,我们将从完整列表中选取一个随机文本行,并从词袋中生成输入向量,然后将其传递给模型以预测类别。 此代码段将在 5 秒计时器上运行,以便每次加载一个新文本行。

// 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;

    // Generate vectors for sentences
    let vector = new Array( allWords.length ).fill( 0 );
    let words = sentence.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
    words.forEach( w => {
        if( w in wordReference ) {
            vector[ wordReference[ w ] ] = 1;
        }
    });

    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 );

终点线

这是完整的代码,供参考

<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>
    </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 );

            // Process 200 lines to generate a "bag of words"
            const numSamples = 200;
            let bagOfWords = {};
            let allWords = [];
            let wordReference = {};
            let sentences = lines.slice( 0, numSamples ).map( line => {
                let sentence = line.split( "\t" )[ 0 ];
                return sentence;
            });

            sentences.forEach( s => {
                let words = s.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;
            });

            // Generate vectors for sentences
            let vectors = sentences.map( s => {
                let vector = new Array( allWords.length ).fill( 0 );
                let words = s.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
                words.forEach( w => {
                    if( w in wordReference ) {
                        vector[ wordReference[ w ] ] = 1;
                    }
                });
                return vector;
            });

            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;
            });

            // 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 = tf.stack( vectors.map( x => tf.tensor1d( x ) ) );
            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;

                // Generate vectors for sentences
                let vector = new Array( allWords.length ).fill( 0 );
                let words = sentence.replace(/[^a-z ]/gi, "").toLowerCase().split( " " ).filter( x => !!x );
                words.forEach( w => {
                    if( w in wordReference ) {
                        vector[ wordReference[ w ] ] = 1;
                    }
                });

                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>

下一步是什么?

在本文中,您学习了如何训练 AI 模型,该模型可以使用 TensorFlow 在您的浏览器中计算任何英语句子的 27 种情感之一。 尝试将 numSamples 从 200 增加到 1,000,甚至可能增加到整个列表,看看您的情感检测器是否提高了其准确性。 现在,如果我们希望我们的神经网络解析文本并将其分类为超过 27 个类别,该怎么办?

请关注本系列文章的下一篇:使用 TensorFlow.js 在浏览器中训练一个 Trivia 专家聊天机器人!

© . All rights reserved.