Русский
Русский
English
Статистика
Реклама

Tensorflow

Перевод Обнаружение эмоций на лице в браузере с помощью глубокого обучения и TensorFlow.js. Часть 2

02.03.2021 20:22:33 | Автор: admin

В предыдущей статье мы узнали, как использовать модели ИИ для определения формы лиц. В этой статье мы используем ключевые ориентиры лица, чтобы получить больше информации о лице из изображений.

В этой статье мы используем ключевые ориентиры лица, чтобы получить больше информации о лице из изображений. Мы используем глубокое обучение на отслеженных лицах из набора данных FER+ и попытаемся точно определить эмоции человека по точкам лица в браузере с помощью TensorFlow.js.

Соединив наш код отслеживания лица с набором данных об эмоциях на лице FER, мы обучим вторую нейросетевую модель определять эмоции человека по нескольким трехмерным ключевым точкам.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии.

Настройка по данным об эмоциях на лице FER2013

Мы используем код для отслеживания лиц из предыдущей статьи, чтобы создать две веб-страницы. Одна страница будет использоваться для обучения модели ИИ на точках отслеженных лиц в наборе данных FER, а другая будет загружать обученную модель и применять её к тестовому набору данных.

Давайте изменим окончательный код из проекта отслеживания лиц, чтобы обучить нейросетевую модель и применить её к данным о лицах. Набор данных FER2013 состоит более чем из 28 тысяч помеченных изображений лиц; он доступен на веб-сайте Kaggle. Мы загрузили эту версию, в которой набор данных уже преобразован в файлы изображений, и поместили её в папку web/fer2013. Затем мы обновили код сервера NodeJS в index.js, чтобы он возвращал список ссылок на изображения по адресу http://localhost:8080/data/. Поэтому вы можете получить полный объект JSON, если запустите сервер локально.

Чтобы упростить задачу, мы сохранили этот объект JSON в файле web/fer2013.js, чтобы вы могли использовать его напрямую, не запуская сервер локально. Вы можете включить его в другие файлы скриптов в верхней части страницы:

<script src="web/fer2013.js"></script>

Мы собираемся работать с изображениями, а не с видео с веб-камеры (не беспокойтесь, мы вернёмся к видео в следующей статье). Поэтому нам нужно заменить элемент<video> элементом <img>и переименовать его ID в image. Мы также можем удалить функцию setupWebcam, так как для этого проекта она не нужна.

<img id="image" style="    visibility: hidden;    width: auto;    height: auto;    "/>

Далее добавим служебную функцию, чтобы задать изображение для элемента, и ещё одну, чтобы перетасовать массив данных. Так как исходные изображения имеют размер всего 48x48 пикселей, давайте для большего выходного размера зададим 500 пикселей, чтобы получить более детальное отслеживание лиц и возможность видеть результат в более крупном элементе canvas. Также обновим служебные функции для линий и многоугольников, чтобы масштабировать в соответствии с выходными данными.

async function setImage( url ) {    return new Promise( res => {        let image = document.getElementById( "image" );        image.src = url;        image.onload = () => {            res();        };    });}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 ] ];    }}const OUTPUT_SIZE = 500;

Нам понадобятся некоторые глобальные переменные: для списка категорий эмоций, списка агрегированных массивов данных FER и индекса массива:

const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];let ferData = [];let setIndex = 0;

Внутри блока async мы можем подготовить и перетасовать данные FER и изменить размер элемента canvas до 500x500 пикселей:

const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );Object.keys( fer2013 ).forEach( em => {    shuffleArray( fer2013[ em ] );    for( let i = 0; i < minSamples; i++ ) {        ferData.push({            emotion: em,            file: fer2013[ em ][ i ]        });    }});shuffleArray( ferData );let canvas = document.getElementById( "output" );canvas.width = OUTPUT_SIZE;canvas.height = OUTPUT_SIZE;

Нам нужно в последний раз обновить шаблон кода перед обучением модели ИИ на одной странице и применением обученной модели на второй странице. Необходимо обновить функцию trackFace, чтобы она работала с элементом image, а не video. Также требуется масштабировать ограничивающий прямоугольник и выходные данные сетки для лица в соответствии с размером элемента canvas. Мы зададим приращение setIndex в конце функции для перехода к следующему изображению.

async function trackFace() {    // Set to the next training image    await setImage( ferData[ setIndex ].file );    const image = document.getElementById( "image" );    const faces = await model.estimateFaces( {        input: image,        returnTensors: false,        flipHorizontal: false,    });    output.drawImage(        image,        0, 0, image.width, image.height,        0, 0, OUTPUT_SIZE, OUTPUT_SIZE    );    const scale = OUTPUT_SIZE / image.width;    faces.forEach( face => {        // Draw the bounding box        const x1 = face.boundingBox.topLeft[ 0 ];        const y1 = face.boundingBox.topLeft[ 1 ];        const x2 = face.boundingBox.bottomRight[ 0 ];        const y2 = face.boundingBox.bottomRight[ 1 ];        const bWidth = x2 - x1;        const bHeight = y2 - y1;        drawLine( output, x1, y1, x2, y1, scale );        drawLine( output, x2, y1, x2, y2, scale );        drawLine( output, x1, y2, x2, y2, scale );        drawLine( output, x1, y1, x1, y2, scale );        // Draw the face mesh        const keypoints = face.scaledMesh;        for( let i = 0; i < FaceTriangles.length / 3; i++ ) {            let pointA = keypoints[ FaceTriangles[ i * 3 ] ];            let pointB = keypoints[ FaceTriangles[ i * 3 + 1 ] ];            let pointC = keypoints[ FaceTriangles[ i * 3 + 2 ] ];            drawTriangle( output, pointA[ 0 ], pointA[ 1 ], pointB[ 0 ], pointB[ 1 ], pointC[ 0 ], pointC[ 1 ], scale );        }    });    setText( `${setIndex + 1}. Face Tracking Confidence: ${face.faceInViewConfidence.toFixed( 3 )} - ${ferData[ setIndex ].emotion}` );    setIndex++;    requestAnimationFrame( trackFace );}

Теперь наш изменённый шаблон готов. Создайте две копии этого кода, чтобы можно было одну страницу задать для глубокого обучения, а другую страницу для тестирования.

1. Глубокое изучение эмоций на лице

В этом первом файле веб-страницы мы собираемся задать обучающие данные, создать нейросетевую модель, а затем обучить её и сохранить веса в файл. В код включена предварительно обученная модель (см. папку web/model), поэтому при желании можно пропустить эту часть и перейти к части2.

Добавьте глобальную переменную для хранения обучающих данных и служебную функцию для преобразования меток эмоций в унитарный вектор, чтобы мы могли использовать его для обучающих данных:

let trainingData = [];function emotionToArray( emotion ) {    let array = [];    for( let i = 0; i < emotions.length; i++ ) {        array.push( emotion === emotions[ i ] ? 1 : 0 );    }    return array;}

Внутри функции trackFace мы возьмём различные ключевые черты лица, масштабируем их относительно размера ограничивающего прямоугольника и добавим их в набор обучающих данных, если значение достоверности отслеживания лица достаточно велико. Мы закомментировали некоторые дополнительные черты лица, чтобы упростить данные, но вы можете добавить их обратно, если хотите поэкспериментировать. Если вы это делаете, не забудьте сопоставить эти функции при применении модели.

// Add just the nose, cheeks, eyes, eyebrows & mouthconst features = [    "noseTip",    "leftCheek",    "rightCheek",    "leftEyeLower1", "leftEyeUpper1",    "rightEyeLower1", "rightEyeUpper1",    "leftEyebrowLower", //"leftEyebrowUpper",    "rightEyebrowLower", //"rightEyebrowUpper",    "lipsLowerInner", //"lipsLowerOuter",    "lipsUpperInner", //"lipsUpperOuter",];let points = [];features.forEach( feature => {    face.annotations[ feature ].forEach( x => {        points.push( ( x[ 0 ] - x1 ) / bWidth );        points.push( ( x[ 1 ] - y1 ) / bHeight );    });});// Only grab the faces that are confidentif( face.faceInViewConfidence > 0.9 ) {    trainingData.push({        input: points,        output: ferData[ setIndex ].emotion,    });}

Скомпилировав достаточное количество обучающих данных, мы можем передать их функции trainNet. В верхней части функции trackFace давайте закончим цикл отслеживания лиц и выйдем из него после 200 изображений и вызовем функцию обучения:

async function trackFace() {    // Fast train on just 200 of the images    if( setIndex >= 200 ) {        setText( "Finished!" );        trainNet();        return;    }    ...}

Наконец, мы пришли к той части, которую так долго ждали: давайте создадим функцию trainNet и обучим нашу модель ИИ!

Эта функция разделит данные обучения на входной массив ключевых точек и выходной массив унитарных векторов эмоций, создаст категорийную модель TensorFlow с несколькими скрытыми слоями, выполнит обучение за 1000 итераций и загрузит обученную модель. Чтобы дополнительно обучить модель, число итераций можно увеличить.

async function trainNet() {    let inputs = trainingData.map( x => x.input );    let outputs = trainingData.map( x => emotionToArray( x.output ) );    // Define our model with several hidden layers    const model = tf.sequential();    model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ inputs[ 0 ].length ] } ) );    model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );    model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );    model.add(tf.layers.dense( {        units: emotions.length,        kernelInitializer: 'varianceScaling',        useBias: false,        activation: "softmax"    } ) );    model.compile({        optimizer: "adam",        loss: "categoricalCrossentropy",        metrics: "acc"    });    const xs = tf.stack( inputs.map( x => tf.tensor1d( x ) ) );    const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );    await model.fit( xs, ys, {        epochs: 1000,        shuffle: true,        callbacks: {            onEpochEnd: ( epoch, logs ) => {                setText( `Training... Epoch #${epoch} (${logs.acc.toFixed( 3 )})` );                console.log( "Epoch #", epoch, logs );            }        }    } );    // Download the trained model    const saveResult = await model.save( "downloads://facemo" );}

На этом всё! На этой веб-странице модель ИИ будет обучена распознавать выражения лиц в различных категориях, и вы получите модель для загрузки и применения. Это мы и сделаем далее.

1. Финишная прямая

Вот полный код обучения модели на наборе данных FER:
<html>    <head>        <title>Training - Recognizing Facial Expressions in the Browser with Deep Learning using TensorFlow.js</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="web/triangles.js"></script>        <script src="web/fer2013.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <img id="image" style="            visibility: hidden;            width: auto;            height: auto;            "/>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        async function setImage( url ) {            return new Promise( res => {                let image = document.getElementById( "image" );                image.src = url;                image.onload = () => {                    res();                };            });        }        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 ] ];            }        }        function drawLine( ctx, x1, y1, x2, y2, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.stroke();        }        function drawTriangle( ctx, x1, y1, x2, y2, x3, y3, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.lineTo( x3 * scale, y3 * scale );            ctx.lineTo( x1 * scale, y1 * scale );            ctx.stroke();        }        const OUTPUT_SIZE = 500;        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let ferData = [];        let setIndex = 0;        let trainingData = [];        let output = null;        let model = null;        function emotionToArray( emotion ) {            let array = [];            for( let i = 0; i < emotions.length; i++ ) {                array.push( emotion === emotions[ i ] ? 1 : 0 );            }            return array;        }        async function trainNet() {            let inputs = trainingData.map( x => x.input );            let outputs = trainingData.map( x => emotionToArray( x.output ) );            // Define our model with several hidden layers            const model = tf.sequential();            model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ inputs[ 0 ].length ] } ) );            model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );            model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );            model.add(tf.layers.dense( {                units: emotions.length,                kernelInitializer: 'varianceScaling',                useBias: false,                activation: "softmax"            } ) );            model.compile({                optimizer: "adam",                loss: "categoricalCrossentropy",                metrics: "acc"            });            const xs = tf.stack( inputs.map( x => tf.tensor1d( x ) ) );            const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );            await model.fit( xs, ys, {                epochs: 1000,                shuffle: true,                callbacks: {                    onEpochEnd: ( epoch, logs ) => {                        setText( `Training... Epoch #${epoch} (${logs.acc.toFixed( 3 )})` );                        console.log( "Epoch #", epoch, logs );                    }                }            } );            // Download the trained model            const saveResult = await model.save( "downloads://facemo" );        }        async function trackFace() {            // Fast train on just 200 of the images            if( setIndex >= 200 ) {//ferData.length ) {                setText( "Finished!" );                trainNet();                return;            }            // Set to the next training image            await setImage( ferData[ setIndex ].file );            const image = document.getElementById( "image" );            const faces = await model.estimateFaces( {                input: image,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                image,                0, 0, image.width, image.height,                0, 0, OUTPUT_SIZE, OUTPUT_SIZE            );            const scale = OUTPUT_SIZE / image.width;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1, scale );                drawLine( output, x2, y1, x2, y2, scale );                drawLine( output, x1, y2, x2, y2, scale );                drawLine( output, x1, y1, x1, y2, scale );                // Draw the face mesh                const keypoints = face.scaledMesh;                for( let i = 0; i < FaceTriangles.length / 3; i++ ) {                    let pointA = keypoints[ FaceTriangles[ i * 3 ] ];                    let pointB = keypoints[ FaceTriangles[ i * 3 + 1 ] ];                    let pointC = keypoints[ FaceTriangles[ i * 3 + 2 ] ];                    drawTriangle( output, pointA[ 0 ], pointA[ 1 ], pointB[ 0 ], pointB[ 1 ], pointC[ 0 ], pointC[ 1 ], scale );                }                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                let points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });                // Only grab the faces that are confident                if( face.faceInViewConfidence > 0.9 ) {                    trainingData.push({                        input: points,                        output: ferData[ setIndex ].emotion,                    });                }            });            setText( `${setIndex + 1}. Face Tracking Confidence: ${face.faceInViewConfidence.toFixed( 3 )} - ${ferData[ setIndex ].emotion}` );            setIndex++;            requestAnimationFrame( trackFace );        }        (async () => {            // Get FER-2013 data from the local web server            // https://www.kaggle.com/msambare/fer2013            // The data can be downloaded from Kaggle and placed inside the "web/fer2013" folder            // Get the lowest number of samples out of all emotion categories            const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );            Object.keys( fer2013 ).forEach( em => {                shuffleArray( fer2013[ em ] );                for( let i = 0; i < minSamples; i++ ) {                    ferData.push({                        emotion: em,                        file: fer2013[ em ][ i ]                    });                }            });            shuffleArray( ferData );            let canvas = document.getElementById( "output" );            canvas.width = OUTPUT_SIZE;            canvas.height = OUTPUT_SIZE;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

2. Обнаружение эмоций на лице

Мы почти достигли своей цели. Применение модели обнаружения эмоций проще, чем её обучение. На этой веб-странице мы собираемся загрузить обученную модель TensorFlow и протестировать её на случайных лицах из набора данных FER.

Мы можем загрузить модель обнаружения эмоций в глобальную переменную прямо под кодом загрузки модели обнаружения ориентиров лица. Обучив свою модель в части1, вы можете обновить путь в соответствии с местом сохранения своей модели.

let emotionModel = null;(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    // Load Emotion Detection    emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );    ...})();

После этого мы можем написать функцию, которая применяет модель к входным данным ключевых точек лица и возвращает название обнаруженной эмоции:

async function predictEmotion( points ) {    let result = tf.tidy( () => {        const xs = tf.stack( [ tf.tensor1d( points ) ] );        return emotionModel.predict( xs );    });    let prediction = await result.data();    result.dispose();    // Get the index of the maximum value    let id = prediction.indexOf( Math.max( ...prediction ) );    return emotions[ id ];}

Чтобы между тестовыми изображениями можно было делать паузу в несколько секунд, давайте создадим служебную функцию wait:

function wait( ms ) {    return new Promise( res => setTimeout( res, ms ) );}

Теперь, чтобы привести ее в действие, мы можем взять ключевые точки отслеженного лица, масштабировать их до ограничивающего прямоугольника для подготовки в качестве входных данных, запустить распознавание эмоций и отобразить ожидаемый и обнаруженный результат с интервалом 2 секунды между изображениями.

async function trackFace() {    ...    let points = null;    faces.forEach( face => {        ...        // Add just the nose, cheeks, eyes, eyebrows & mouth        const features = [            "noseTip",            "leftCheek",            "rightCheek",            "leftEyeLower1", "leftEyeUpper1",            "rightEyeLower1", "rightEyeUpper1",            "leftEyebrowLower", //"leftEyebrowUpper",            "rightEyebrowLower", //"rightEyebrowUpper",            "lipsLowerInner", //"lipsLowerOuter",            "lipsUpperInner", //"lipsUpperOuter",        ];        points = [];        features.forEach( feature => {            face.annotations[ feature ].forEach( x => {                points.push( ( x[ 0 ] - x1 ) / bWidth );                points.push( ( x[ 1 ] - y1 ) / bHeight );            });        });    });    if( points ) {        let emotion = await predictEmotion( points );        setText( `${setIndex + 1}. Expected: ${ferData[ setIndex ].emotion} vs. ${emotion}` );    }    else {        setText( "No Face" );    }    setIndex++;    await wait( 2000 );    requestAnimationFrame( trackFace );}

Готово! Наш код должен начать определять эмоции на изображениях FER в соответствии с ожидаемой эмоцией. Попробуйте, и увидите, как он работает.

2. Финишная прямая

Взгляните на полный код применения обученной модели к изображениям из набора данных FER:
<html>    <head>        <title>Running - Recognizing Facial Expressions in the Browser with Deep Learning using TensorFlow.js</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="web/fer2013.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <img id="image" style="            visibility: hidden;            width: auto;            height: auto;            "/>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        async function setImage( url ) {            return new Promise( res => {                let image = document.getElementById( "image" );                image.src = url;                image.onload = () => {                    res();                };            });        }        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 ] ];            }        }        function drawLine( ctx, x1, y1, x2, y2, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.stroke();        }        function drawTriangle( ctx, x1, y1, x2, y2, x3, y3, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.lineTo( x3 * scale, y3 * scale );            ctx.lineTo( x1 * scale, y1 * scale );            ctx.stroke();        }        function wait( ms ) {            return new Promise( res => setTimeout( res, ms ) );        }        const OUTPUT_SIZE = 500;        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let ferData = [];        let setIndex = 0;        let emotionModel = null;        let output = null;        let model = null;        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            // Set to the next training image            await setImage( ferData[ setIndex ].file );            const image = document.getElementById( "image" );            const faces = await model.estimateFaces( {                input: image,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                image,                0, 0, image.width, image.height,                0, 0, OUTPUT_SIZE, OUTPUT_SIZE            );            const scale = OUTPUT_SIZE / image.width;            let points = null;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1, scale );                drawLine( output, x2, y1, x2, y2, scale );                drawLine( output, x1, y2, x2, y2, scale );                drawLine( output, x1, y1, x1, y2, scale );                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `${setIndex + 1}. Expected: ${ferData[ setIndex ].emotion} vs. ${emotion}` );            }            else {                setText( "No Face" );            }            setIndex++;            await wait( 2000 );            requestAnimationFrame( trackFace );        }        (async () => {            // Get FER-2013 data from the local web server            // https://www.kaggle.com/msambare/fer2013            // The data can be downloaded from Kaggle and placed inside the "web/fer2013" folder            // Get the lowest number of samples out of all emotion categories            const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );            Object.keys( fer2013 ).forEach( em => {                shuffleArray( fer2013[ em ] );                for( let i = 0; i < minSamples; i++ ) {                    ferData.push({                        emotion: em,                        file: fer2013[ em ][ i ]                    });                }            });            shuffleArray( ferData );            let canvas = document.getElementById( "output" );            canvas.width = OUTPUT_SIZE;            canvas.height = OUTPUT_SIZE;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Позволит ли это определять наши эмоции на лице?

В этой статье мы объединили выходные данные модели обнаружения ориентиров лица TensorFlow с независимым набором данных, чтобы создать новую модель, которая позволяет извлекать из изображения больше информации, чем раньше. Настоящей проверкой стало бы применение этой новой модели для определения эмоций на любом лице.

В следующей статье этой серии мы, используя полученное с веб-камеры видео нашего лица, узнаем, сможет ли модель реагировать на выражение лица в реальном времени. До встречи завтра, в это же время.

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Подробнее..

Перевод Обнаружение эмоций на лице в реальном времени с помощью веб-камеры в браузере с использованием TensorFlow.js. Часть 3

03.03.2021 22:15:39 | Автор: admin

Мы уже научились использовать искусственный интеллект (ИИ) в веб-браузере для отслеживания лиц в реальном времени и применять глубокое обучение для обнаружения и классификации эмоций на лице. Итак, мы собрали эти два компонента вместе и хотим узнать, сможем ли мы в реальном времени обнаруживать эмоции с помощью веб-камеры. В этой статье мы, используя транслируемое с веб-камеры видео нашего лица, узнаем, сможет ли модель реагировать на выражение лица в реальном времени.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Добавление обнаружения эмоций на лице

В этом проекте мы протестируем нашу обученную модель обнаружения эмоций на лице на видео, транслируемом с веб-камеры. Мы начнём со стартового шаблона с окончательным кодом из проекта отслеживания лиц и внесём в него части кода для обнаружения эмоций на лице.

Давайте загрузим и применим нашу предварительно обученную модель выражений на лице. Начала мы определим некоторые глобальные переменные для обнаружения эмоций, как мы делали раньше:

const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];let emotionModel = null;

Затем мы можем загрузить модель обнаружения эмоций внутри блока async:

(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    // Load Emotion Detection    emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );    ...})();

А для модельного прогнозирования по ключевым точкам лица мы можем добавить служебную функцию:

async function predictEmotion( points ) {    let result = tf.tidy( () => {        const xs = tf.stack( [ tf.tensor1d( points ) ] );        return emotionModel.predict( xs );    });    let prediction = await result.data();    result.dispose();    // Get the index of the maximum value    let id = prediction.indexOf( Math.max( ...prediction ) );    return emotions[ id ];}

Наконец, нам нужно получить ключевые точки лица от модуля обнаружения внутри функции trackFace и передать их модулю прогнозирования эмоций.

async function trackFace() {    ...    let points = null;    faces.forEach( face => {        ...        // Add just the nose, cheeks, eyes, eyebrows & mouth        const features = [            "noseTip",            "leftCheek",            "rightCheek",            "leftEyeLower1", "leftEyeUpper1",            "rightEyeLower1", "rightEyeUpper1",            "leftEyebrowLower", //"leftEyebrowUpper",            "rightEyebrowLower", //"rightEyebrowUpper",            "lipsLowerInner", //"lipsLowerOuter",            "lipsUpperInner", //"lipsUpperOuter",        ];        points = [];        features.forEach( feature => {            face.annotations[ feature ].forEach( x => {                points.push( ( x[ 0 ] - x1 ) / bWidth );                points.push( ( x[ 1 ] - y1 ) / bHeight );            });        });    });    if( points ) {        let emotion = await predictEmotion( points );        setText( `Detected: ${emotion}` );    }    else {        setText( "No Face" );    }    requestAnimationFrame( trackFace );}

Это всё, что нужно для достижения нужной цели. Теперь, когда вы открываете веб-страницу, она должна обнаружить ваше лицо и распознать эмоции. Экспериментируйте и получайте удовольствие!

Вот полный код, нужный для завершения этого проекта
<html>    <head>        <title>Real-Time Facial Emotion Detection</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let emotionModel = null;        let output = null;        let model = null;        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            const video = document.querySelector( "video" );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            let points = null;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1 );                drawLine( output, x2, y1, x2, y2 );                drawLine( output, x1, y2, x2, y2 );                drawLine( output, x1, y1, x1, y2 );                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `Detected: ${emotion}` );            }            else {                setText( "No Face" );            }            requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Когда мы сможем носить виртуальные очки?

Взяв код из первых двух статей этой серии, мы смогли создать детектор эмоций на лице в реальном времени, используя лишь немного кода на JavaScript. Только представьте, что ещё можно сделать с помощью библиотеки TensorFlow.js! В следующей статье мы вернёмся к нашей цели создать фильтр для лица в стиле Snapchat, используя то, что мы уже узнали об отслеживании лиц и добавлении 3D-визуализации посредством ThreeJS. Оставайтесь с нами! До встречи завтра, в это же время!

Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 2

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Подробнее..

Перевод Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 4

04.03.2021 20:15:20 | Автор: admin

В 4 части (вы же прочли первую, вторую и третью, да?) мы возвращаемся к нашей цели создание фильтра для лица в стиле Snapchat, используя то, что мы уже узнали об отслеживании лиц и добавлении 3D-визуализации посредством ThreeJS. В этой статье мы собираемся использовать ключевые точки лица для виртуальной визуализации 3D-модели поверх видео с веб-камеры, чтобы немного развлечься с дополненной реальностью.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Добавление 3D-графики с помощью ThreeJS

Этот проект будет основан на коде проекта отслеживания лиц, который мы создали в начале этой серии. Мы добавим наложение 3D-сцены на исходное полотно.

ThreeJS позволяет относительно легко работать с 3D-графикой, поэтому мы собираемся с помощью этой библиотеки визуализировать виртуальные очки поверх наших лиц.

В верхней части страницы нам нужно включить два файла скриптов, чтобы добавить ThreeJS и загрузчик файлов в формате GLTF для модели виртуальных очков, которую мы будем использовать:

<script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/build/three.min.js"></script><script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script>

Чтобы упростить задачу и не беспокоиться о том, как поместить текстуру веб-камеры на сцену, мы можем наложить дополнительное прозрачное полотно (canvas) и нарисовать виртуальные очки на нём. Мы используем CSS-код, приведённый ниже над тегом body, поместив выходное полотно (output) в контейнер и добавив полотно наложения (overlay).

<style>    .canvas-container {        position: relative;        width: auto;        height: auto;    }    .canvas-container canvas {        position: absolute;        left: 0;        width: auto;        height: auto;    }</style><body>    <div class="canvas-container">        <canvas id="output"></canvas>        <canvas id="overlay"></canvas>    </div>    ...</body>

Для 3D-сцены требуется несколько переменных, и мы можем добавить служебную функцию загрузки 3D-модели для файлов GLTF:

<style>    .canvas-container {        position: relative;        width: auto;        height: auto;    }    .canvas-container canvas {        position: absolute;        left: 0;        width: auto;        height: auto;    }</style><body>    <div class="canvas-container">        <canvas id="output"></canvas>        <canvas id="overlay"></canvas>    </div>    ...</body>

Теперь мы можем инициализировать все компоненты нашего блока async, начиная с размера полотна наложения, как это было сделано с выходным полотном:

(async () => {    ...    let canvas = document.getElementById( "output" );    canvas.width = video.width;    canvas.height = video.height;    let overlay = document.getElementById( "overlay" );    overlay.width = video.width;    overlay.height = video.height;    ...})();

Также необходимо задать переменные renderer, scene и camera. Даже если вы не знакомы с трёхмерной перспективой и математикой камеры, вам не надо волноваться. Этот код просто располагает камеру сцены так, чтобы ширина и высота видео веб-камеры соответствовали координатам трёхмерного пространства:

(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    renderer = new THREE.WebGLRenderer({        canvas: document.getElementById( "overlay" ),        alpha: true    });    camera = new THREE.PerspectiveCamera( 45, 1, 0.1, 2000 );    camera.position.x = videoWidth / 2;    camera.position.y = -videoHeight / 2;    camera.position.z = -( videoHeight / 2 ) / Math.tan( 45 / 2 ); // distance to z should be tan( fov / 2 )    scene = new THREE.Scene();    scene.add( new THREE.AmbientLight( 0xcccccc, 0.4 ) );    camera.add( new THREE.PointLight( 0xffffff, 0.8 ) );    scene.add( camera );    camera.lookAt( { x: videoWidth / 2, y: -videoHeight / 2, z: 0, isVector3: true } );    ...})();

Нам нужно добавить в функцию trackFace всего лишь одну строку кода для визуализации сцены поверх выходных данных отслеживания лица:

async function trackFace() {    const video = document.querySelector( "video" );    output.drawImage(        video,        0, 0, video.width, video.height,        0, 0, video.width, video.height    );    renderer.render( scene, camera );    const faces = await model.estimateFaces( {        input: video,        returnTensors: false,        flipHorizontal: false,    });    ...}

Последний этап этого ребуса перед отображением виртуальных объектов на нашем лице загрузка 3D-модели виртуальных очков. Мы нашли пару очков в форме сердца от Maximkuzlin на SketchFab. При желании вы можете загрузить и использовать другой объект.

Здесь показано, как загрузить объект и добавить его в сцену до вызова функции trackFace:

Размещение виртуальных очков на отслеживаемом лице

Теперь начинается самое интересное наденем наши виртуальные очки.

Помеченные аннотации, предоставляемые моделью отслеживания лиц TensorFlow, включают массив координат MidwayBetweenEyes, в котором координаты X и Y соответствуют экрану, а координата Z добавляет экрану глубины. Это делает размещение очков на наших глазах довольно простой задачей.

Необходимо сделать координату Y отрицательной, так как в системе координат двумерного экрана положительная ось Y направлена вниз, но в пространственной системе координат указывает вверх. Мы также вычтем из значения координаты Z расстояние или глубину камеры, чтобы получить правильное расстояния в сцене.

glasses.position.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ];glasses.position.y = -face.annotations.midwayBetweenEyes[ 0 ][ 1 ];glasses.position.z = -camera.position.z + face.annotations.midwayBetweenEyes[ 0 ][ 2 ];

Теперь нужно рассчитать ориентацию и масштаб очков. Это возможно, если мы определим направление вверх относительно нашего лица, которое указывает на макушку нашей головы, и расстояние между глазами.

Оценить направление вверх можно с помощью вектора из массива midwayBetweenEyes, использованного для очков, вместе с отслеживаемой точкой для нижней части носа. Затем нормируем его длину следующим образом:

glasses.up.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];glasses.up.y = -( face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ] );glasses.up.z = face.annotations.midwayBetweenEyes[ 0 ][ 2 ] - face.annotations.noseBottom[ 0 ][ 2 ];const length = Math.sqrt( glasses.up.x ** 2 + glasses.up.y ** 2 + glasses.up.z ** 2 );glasses.up.x /= length;glasses.up.y /= length;glasses.up.z /= length;

Чтобы получить относительный размер головы, можно вычислить расстояние между глазами:

const eyeDist = Math.sqrt(    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2);

Наконец, мы масштабируем очки на основе значения eyeDist и ориентируем очки по оси Z, используя угол между вектором вверх и осью Y. И вуаля!

Выполните свой код и проверьте результат.

Прежде чем перейти к следующей части этой серии, давайте посмотрим на полный код, собранный вместе:

Простыня с кодом
<html>    <head>        <title>Creating a Snapchat-Style Virtual Glasses Face Filter</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/build/three.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script>    </head>    <style>        .canvas-container {            position: relative;            width: auto;            height: auto;        }        .canvas-container canvas {            position: absolute;            left: 0;            width: auto;            height: auto;        }    </style>    <body>        <div class="canvas-container">            <canvas id="output"></canvas>            <canvas id="overlay"></canvas>        </div>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        let output = null;        let model = null;        let renderer = null;        let scene = null;        let camera = null;        let glasses = null;        function loadModel( file ) {            return new Promise( ( res, rej ) => {                const loader = new THREE.GLTFLoader();                loader.load( file, function ( gltf ) {                    res( gltf.scene );                }, undefined, function ( error ) {                    rej( error );                } );            });        }        async function trackFace() {            const video = document.querySelector( "video" );            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            renderer.render( scene, camera );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1 );                drawLine( output, x2, y1, x2, y2 );                drawLine( output, x1, y2, x2, y2 );                drawLine( output, x1, y1, x1, y2 );                glasses.position.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ];                glasses.position.y = -face.annotations.midwayBetweenEyes[ 0 ][ 1 ];                glasses.position.z = -camera.position.z + face.annotations.midwayBetweenEyes[ 0 ][ 2 ];                // Calculate an Up-Vector using the eyes position and the bottom of the nose                glasses.up.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];                glasses.up.y = -( face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ] );                glasses.up.z = face.annotations.midwayBetweenEyes[ 0 ][ 2 ] - face.annotations.noseBottom[ 0 ][ 2 ];                const length = Math.sqrt( glasses.up.x ** 2 + glasses.up.y ** 2 + glasses.up.z ** 2 );                glasses.up.x /= length;                glasses.up.y /= length;                glasses.up.z /= length;                // Scale to the size of the head                const eyeDist = Math.sqrt(                    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                );                glasses.scale.x = eyeDist / 6;                glasses.scale.y = eyeDist / 6;                glasses.scale.z = eyeDist / 6;                glasses.rotation.y = Math.PI;                glasses.rotation.z = Math.PI / 2 - Math.acos( glasses.up.x );            });            requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            let overlay = document.getElementById( "overlay" );            overlay.width = video.width;            overlay.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            renderer = new THREE.WebGLRenderer({                canvas: document.getElementById( "overlay" ),                alpha: true            });            camera = new THREE.PerspectiveCamera( 45, 1, 0.1, 2000 );            camera.position.x = videoWidth / 2;            camera.position.y = -videoHeight / 2;            camera.position.z = -( videoHeight / 2 ) / Math.tan( 45 / 2 ); // distance to z should be tan( fov / 2 )            scene = new THREE.Scene();            scene.add( new THREE.AmbientLight( 0xcccccc, 0.4 ) );            camera.add( new THREE.PointLight( 0xffffff, 0.8 ) );            scene.add( camera );            camera.lookAt( { x: videoWidth / 2, y: -videoHeight / 2, z: 0, isVector3: true } );            // Glasses from https://sketchfab.com/3d-models/heart-glasses-ef812c7e7dc14f6b8783ccb516b3495c            glasses = await loadModel( "web/3d/heart_glasses.gltf" );            scene.add( glasses );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Что если также добавить обнаружение эмоций на лице?

Поверите ли, что всё это возможно на одной веб-странице? Добавив 3D-объекты к функции отслеживания лиц в реальном времени, мы сотворили волшебство с помощью камеры прямо в веб-браузере. Вы можете подумать: Но очки в форме сердца существуют в реальной жизни И это правда! А что, если мы создадим что-то действительно волшебное, например шляпу которая знает, что мы чувствуем?

Давайте в следующей статье создадим волшебную шляпу (как в Хогвартсе!) для обнаружения эмоций и посмотрим, сможем ли мы сделать невозможное возможным, ещё больше используя библиотеку TensorFlow.js! До встречи завтра, в это же время.

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Перевод Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 5

06.03.2021 20:20:06 | Автор: admin

Носить виртуальные аксессуары это весело, но до их ношения в реальной жизни всего один шаг. Мы могли бы легко создать приложение, которое позволяет виртуально примерять шляпы именно такое приложение вы могли бы захотеть создать для веб-сайта электронной коммерции. Но, если мы собираемся это сделать, почему бы при этом не получить немного больше удовольствия? Программное обеспечение замечательно тем, что мы можем воплотить в жизнь своё воображение.

В этой статье мы собираемся соединить все предыдущие части, чтобы создать волшебную шляпу для обнаружения эмоций, которая распознаёт и реагирует на выражения лиц, когда мы носим её виртуально.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Создание волшебной шляпы

Помните, как мы ранее в этой серии статей создавали функцию обнаружения эмоций на лице в реальном времени? Теперь давайте добавим немного графики в этот проект придадим ему, так сказать, лицо.

Чтобы создать нашу виртуальную шляпу, мы собираемся добавить графические ресурсы на веб-страницу как скрытые элементы img:

<img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" /><img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" /><img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" /><img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" /><img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" /><img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" /><img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />

Ключевое свойство этого проекта заключается в том, что шляпа должна отображаться всё время, в правильном положении и с правильным размером, поэтому мы сохраним состояния шляпы в глобальной переменной:

let currentEmotion = "neutral";let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };

Рисовать шляпу этого размера и в этом положении мы будем с помощью 2D-преобразования полотна в каждом кадре.

async function trackFace() {    ...    output.drawImage(        video,        0, 0, video.width, video.height,        0, 0, video.width, video.height    );    let hatImage = document.getElementById( `hat-${currentEmotion}` );    output.save();    output.translate( -hatImage.width / 2, -hatImage.height / 2 );    output.translate( hat.position.x, hat.position.y );    output.drawImage(        hatImage,        0, 0, hatImage.width, hatImage.height,        0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale    );    output.restore();    ...}

По ключевым точкам лица, предоставляемым TensorFlow, мы можем рассчитать размер и положение шляпы относительно лица, чтобы задать указанные выше значения.

Размер головы можно оценить по расстоянию между глазами. Вектор вверх аппроксимируем по точке midwayBetweenEyes и точке noseBottom, которые можно использовать для перемещения шляпы вверх ближе к верхней части лица (в отличие от виртуальных очков из предыдущей статьи).

const eyeDist = Math.sqrt(    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2);const faceScale = eyeDist / 80;let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ];const length = Math.sqrt( upX ** 2 + upY ** 2 );upX /= length;upY /= length;hat = {    scale: faceScale,    position: {        x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,        y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,    }};

После сохранения названия спрогнозированной эмоции в currentEmotion отображается соответствующее изображение шляпы, и мы готовы её примерить!

if( points ) {    let emotion = await predictEmotion( points );    setText( `Detected: ${emotion}` );    currentEmotion = emotion;}else {    setText( "No Face" );}
Вот полный код этого проекта
<html>    <head>        <title>Building a Magical Emotion Detection Hat</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" />        <img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" />        <img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" />        <img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" />        <img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" />        <img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" />        <img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let emotionModel = null;        let output = null;        let model = null;        let currentEmotion = "neutral";        let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            const video = document.querySelector( "video" );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            let hatImage = document.getElementById( `hat-${currentEmotion}` );            output.save();            output.translate( -hatImage.width / 2, -hatImage.height / 2 );            output.translate( hat.position.x, hat.position.y );            output.drawImage(                hatImage,                0, 0, hatImage.width, hatImage.height,                0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale            );            output.restore();            let points = null;            faces.forEach( face => {                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });                const eyeDist = Math.sqrt(                    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                );                const faceScale = eyeDist / 80;                let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];                let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ];                const length = Math.sqrt( upX ** 2 + upY ** 2 );                upX /= length;                upY /= length;                hat = {                    scale: faceScale,                    position: {                        x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,                        y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,                    }                };            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `Detected: ${emotion}` );                currentEmotion = emotion;            }            else {                setText( "No Face" );            }                        requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Возможен ли контроль по состоянию глаз и рта?

В этом проекте собраны воедино все куски, созданные ранее в этой серии статей в целях развлечения с визуальными образами. А что, если бы можно было реализовать в нём взаимодействие с лицом?

В следующей, заключительной статье этой серии мы реализуем обнаружение моргания глаз и открывания рта, чтобы получить интерактивную сцену. Оставайтесь с нами и до встречи завтра, в это же время.

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Перевод Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 6

07.03.2021 22:07:07 | Автор: admin
Активация экранной магии вашим лицом в браузереАктивация экранной магии вашим лицом в браузере

Вот и финал этой серии статей (ссылки на предыдущие части в конце этого материала), в которой мы создавали в браузере фильтры в стиле Snapchat, обучая модель ИИ понимать выражения лиц и добились ещё большего, используя библиотеку Tensorflow.js и отслеживание лиц.

Было бы здорово закончить, реализовав обнаружение движения на лицах? Позвольте показать, как по ключевым точкам лица определять, когда мы открываем рот и моргаем глазами, чтобы активировать события, происходящие на экране.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Обнаружение моргания глаз и открывания рта

Мы собираемся использовать ключевые точки лица, предоставляемые кодом отслеживания лиц, который мы разработали в первой статье этой серии, а также отслеживание лица в реальном времени, чтобы обнаружить моргание глаз и открывание рта.

Аннотированные точки лица дают достаточно информации, чтобы определить, когда глаза закрыты и когда открыт рот. Хитрость заключается в том, чтобы масштабировать положения с учетом относительного размера в анфас.

Для этого мы можем обратиться к удобному расстоянию между глазами, чтобы аппроксимировать относительную шкалу в функции trackFace:

async function trackFace() {    ...    faces.forEach( face => {        const eyeDist = Math.sqrt(            ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +            ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +            ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2        );        const faceScale = eyeDist / 80;    });    requestAnimationFrame( trackFace );}

Затем мы можем вычислить расстояние между верхней и нижней частью как левого, так и правого глаза и использовать значение faceScale для оценки момента пересечения порога. Мы можем использовать аналогичный расчёт для обнаружения открывания рта.

Взгляните:

async function trackFace() {    ...    let areEyesClosed = false, isMouthOpen = false;    faces.forEach( face => {        ...        // Check for eyes closed        const leftEyesDist = Math.sqrt(            ( face.annotations.leftEyeLower1[ 4 ][ 0 ] - face.annotations.leftEyeUpper1[ 4 ][ 0 ] ) ** 2 +            ( face.annotations.leftEyeLower1[ 4 ][ 1 ] - face.annotations.leftEyeUpper1[ 4 ][ 1 ] ) ** 2 +            ( face.annotations.leftEyeLower1[ 4 ][ 2 ] - face.annotations.leftEyeUpper1[ 4 ][ 2 ] ) ** 2        );        const rightEyesDist = Math.sqrt(            ( face.annotations.rightEyeLower1[ 4 ][ 0 ] - face.annotations.rightEyeUpper1[ 4 ][ 0 ] ) ** 2 +            ( face.annotations.rightEyeLower1[ 4 ][ 1 ] - face.annotations.rightEyeUpper1[ 4 ][ 1 ] ) ** 2 +            ( face.annotations.rightEyeLower1[ 4 ][ 2 ] - face.annotations.rightEyeUpper1[ 4 ][ 2 ] ) ** 2        );        if( leftEyesDist / faceScale < 23.5 ) {            areEyesClosed = true;        }        if( rightEyesDist / faceScale < 23.5 ) {            areEyesClosed = true;        }        // Check for mouth open        const lipsDist = Math.sqrt(            ( face.annotations.lipsLowerInner[ 5 ][ 0 ] - face.annotations.lipsUpperInner[ 5 ][ 0 ] ) ** 2 +            ( face.annotations.lipsLowerInner[ 5 ][ 1 ] - face.annotations.lipsUpperInner[ 5 ][ 1 ] ) ** 2 +            ( face.annotations.lipsLowerInner[ 5 ][ 2 ] - face.annotations.lipsUpperInner[ 5 ][ 2 ] ) ** 2        );        // Scale to the relative face size        if( lipsDist / faceScale > 20 ) {            isMouthOpen = true;        }    });    setText( `Eyes: ${areEyesClosed} Mouth: ${isMouthOpen}` );    requestAnimationFrame( trackFace );}

Теперь мы готовы к обнаружению некоторых движений на лицах.

Время вечеринки с конфетти

На каждом празднике требуется конфетти, верно? Мы собираемся соединить виртуальное конфетти с моргающими глазами и открывающимся ртом, чтобы получилась настоящая вечеринка.

Для этого мы будем использовать библиотеку JavaScript с открытым исходным кодом, которая называется Party-JS. Включите её в верхней части своей страницы следующим образом:

<script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/party-js@1.0.0/party.min.js"></script>

Давайте зададим глобальную переменную, по состоянию которой будем отслеживать запуск конфетти.

let didParty = false;

И последнее, но не менее важное: мы можем включать анимацию вечеринки, когда мы моргаем или открываем рот.

async function trackFace() {    ...    if( !didParty && ( areEyesClosed || isMouthOpen ) ) {        party.screen();    }    didParty = areEyesClosed || isMouthOpen;    requestAnimationFrame( trackFace );}

А теперь время для вечеринки! Используя возможности отслеживания лиц и конфетти, вы запускаете вечеринку на экране по движению своих губ.

Этот проект не закончен без полного кода, на который вы могли бы взглянуть. Поэтому вот он:

Простыня с кодом
<html>    <head>        <title>Tracking Faces in the Browser with TensorFlow.js</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/party-js@1.0.0/party.min.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        let output = null;        let model = null;        let didParty = false;        async function trackFace() {            const video = document.getElementById( "webcam" );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            let areEyesClosed = false, isMouthOpen = false;            faces.forEach( face => {                const eyeDist = Math.sqrt(                    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                );                const faceScale = eyeDist / 80;                // Check for eyes closed                const leftEyesDist = Math.sqrt(                    ( face.annotations.leftEyeLower1[ 4 ][ 0 ] - face.annotations.leftEyeUpper1[ 4 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeLower1[ 4 ][ 1 ] - face.annotations.leftEyeUpper1[ 4 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeLower1[ 4 ][ 2 ] - face.annotations.leftEyeUpper1[ 4 ][ 2 ] ) ** 2                );                const rightEyesDist = Math.sqrt(                    ( face.annotations.rightEyeLower1[ 4 ][ 0 ] - face.annotations.rightEyeUpper1[ 4 ][ 0 ] ) ** 2 +                    ( face.annotations.rightEyeLower1[ 4 ][ 1 ] - face.annotations.rightEyeUpper1[ 4 ][ 1 ] ) ** 2 +                    ( face.annotations.rightEyeLower1[ 4 ][ 2 ] - face.annotations.rightEyeUpper1[ 4 ][ 2 ] ) ** 2                );                if( leftEyesDist / faceScale < 23.5 ) {                    areEyesClosed = true;                }                if( rightEyesDist / faceScale < 23.5 ) {                    areEyesClosed = true;                }                // Check for mouth open                const lipsDist = Math.sqrt(                    ( face.annotations.lipsLowerInner[ 5 ][ 0 ] - face.annotations.lipsUpperInner[ 5 ][ 0 ] ) ** 2 +                    ( face.annotations.lipsLowerInner[ 5 ][ 1 ] - face.annotations.lipsUpperInner[ 5 ][ 1 ] ) ** 2 +                    ( face.annotations.lipsLowerInner[ 5 ][ 2 ] - face.annotations.lipsUpperInner[ 5 ][ 2 ] ) ** 2                );                // Scale to the relative face size                if( lipsDist / faceScale > 20 ) {                    isMouthOpen = true;                }            });            if( !didParty && ( areEyesClosed || isMouthOpen ) ) {                party.screen();            }            didParty = areEyesClosed || isMouthOpen;            setText( `Eyes: ${areEyesClosed} Mouth: ${isMouthOpen}` );            requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше?

Собственно, на этом пока всё. В этой серии статей мы научились применять ИИ к лицам, чтобы отслеживать их в режиме реального времени, а также определять эмоции на лице и движения рта и глаз. Мы даже создали с нуля собственную игру с дополненной реальностью и виртуальными очками, и всё это работает в веб-браузере.

Хотя мы выбрали для применения забавные примеры, для этой технологии также существует множество приложений в бизнесе. Представьте продавца очков, который хочет позволить посетителям своего веб-сайта выбирать очки, примеряя их. Нетрудно представить, как вы будете использовать знания, приобретённые в этой серии статей, для создания нужных функциональных возможностей. Надеюсь, теперь у вас есть инструменты для создания более полезных решений с использованием ИИ и TensorFlow.js.

Попробуйте реализовать конфетти в проекте виртуальных очков. Проверьте, сможете ли вы применить обнаружение эмоций к фотоальбому.

И если эти серии статей вдохновят вас на создание ещё более крутых проектов, поделитесь ими в комментариях! Мы будем рады узнать о ваших проектах.

Удачи и удовольствия от программирования!

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Как машинное обучение и TensorFlow помогают готовить гибридную выпечку хобби-кейс разработчика Google

10.02.2021 14:12:50 | Автор: admin

Вынужденная самоизоляция стимулировала многих из нас вспомнить о своих pet-проектах или просто найти себе хобби. Кто-то увлекается радиосвязью, кто-то разрабатывает корпуса для Raspberry. Ну а кто-то занимается выпечкой. Но не простой, а с привлечением машинного обучения.

Разработчик Сара Робинсон, специалист по машинному обучению, решила испечь идеальный кекс. Но не методом проб и ошибок этим занимались наши бабушки, а при помощи технологий. Все началось с того, что взяла 33 разных рецепта печенья, пирогов и хлеба и построила TensorFlow модель для анализа всех этих данных. Сначала целью было понять, почему хлебобулочные изделия иногда сильно крошатся и как этого можно избежать. Но в конечном итоге Сара смогла получить рецепт идеального кекса, который на самом деле является чем-то средним между печеньем и пирогом. А еще рецепт гибрида хлеба и печенек.

От набора данных до кухонного стола


В декабре 2020 года Сара привлекла к проекту своего коллегу сотрудника Google по имени Дейл Маркович. Вместе они и разработали гибридный рецепт. Полученная в итоге модель позволяла по введенным игредиентам определить, что получится в итоге печенье, пирог или хлеб.


После того, как все получилось, разработчики (немного странно употреблять этот термин в применении к выпечке, правда?) решили пойти дальше. Проект масштабировали. На этот раз для анализа отобрали уже 600 рецептов. Их тщательно проанализировали для того, чтобы выделить 16 самых важных ингредиентов, которые влияют на текстуру и упругость выпечки, плюс, конечно, на вкусовые качества.

Этими ингредиентами оказались:

  • дрожжи,
  • мука,
  • сахар,
  • яйца,
  • жир (любое масло),
  • молоко,
  • пищевая сода,
  • разрыхлитель,
  • яблочный уксус,
  • пахта,
  • банан,
  • тыквенное пюре,
  • авокадо,
  • вода,
  • масло,
  • соль.

Авторы проекта при помощи новой модели составили не только список ингредиентов, но и определили правильные пропорции, которые помогают создать идеальную выпечку.


Кроме того, модель оказалась способной самостоятельно определять тип выпечки, отделяя мух от котлет печенье от пирожных, и хлеба. На этом этапе разработчики использовали инструмент Googles AutoML Tables, который позволяет быстро строить модели на основе табличных данных. Они загрузили в модель CSV файл и проанализировали его, проверив свою модель.

Для каждого из типов выпечки печенья, пирога или хлеба модель предсказывала оптимальное количество и соотношение масла, сахара, дрожжей и яиц. А еще полученная модель давала возможность получить рецепт для гибридных блюд. Ниже фото гибрида пирога и печенья с использованием шоколадной крошки.


Пример кода, модель и работающий сервис


Что касается TensorFlow модели, то код достаточно короткий. Для модели использовался Keras API.

model = tf.keras.Sequential([  tf.keras.layers.Dense(16, input_shape=(num_ingredients,)),  tf.keras.layers.Dense(16, activation='relu'),  tf.keras.layers.Dense(3, activation='softmax')                ])

При помощи Python разработчики создали функцию, которая преобразовывает вводимые ингредиенты сначала в привычные для любителей выпечки единицы измерения (чашки, чайные ложки и т.п.), а потом в проценты.

def get_prediction(request):     data = request.get_json()    prescaled = dict(zip(columns, data))    scaled = scale_data(prescaled)        # Send scaled inputs to the model    prediction = predict_json('gcp-project-name', 'baking', scaled)        # Get the item with the highest confidence prediction    predicted_ind = np.argmax(prediction)    label_map = ['Bread', 'Cake', 'Cookies']    baked_prediction = label_map[predicted_ind]    confidence = str(round(prediction[predicted_ind] * 100))     if baked_prediction == 'Bread':        emoji = "It's bread! "    elif baked_prediction == 'Cake':        emoji = "It's cake! 
Подробнее..

Перевод Как преобразовать текст в речь с использованием Google Tesseract и Arm NN на Raspberry Pi

17.02.2021 16:08:10 | Автор: admin

Привет, Хабр! Сегодня специально к старту нового потока курса по Maсhine Learning делимся с вами постом, автор которого создаёт устройство преобразования текста в речь. Такой механизм преобразования текста в речь (TTS) ключевой элемент систем, которые стремятся сформировать естественное взаимодействие между людьми и машинами на основе встроенных устройств. Встроенные устройства могут, например, помочь людям с нарушениями зрения читать знаки, буквы и документы. В частности, устройство может, используя оптическое распознавание символов, дать понять пользователю, что видно на изображении. Впрочем, приступим к крафту



Приложения TTS уже много лет доступны на настольных компьютерах и широко используются на большинстве современных смартфонов и мобильных устройств. Такие приложения можно найти среди средств специальных возможностей в операционной системе, кроме того, широко применяются для чтения с экрана, пользовательских оповещений и многого другого.

Обычно такие системы начинаются с некоторого машиночитаемого текста. Что делать, если у вас нет готового источника текста для документа, браузера или приложения? Программное обеспечение для оптического распознавания символов (OCR) может преобразовывать отсканированные изображения в текст. В контексте приложения TTS это глифы отдельные символы. Программное обеспечение OCR само по себе занимается только точным извлечением цифр и букв.

Для точного обнаружения текста в реальном времени распознавания наборов глифов как произносимых слов можно обратиться к методам глубокого обучения ИИ. В этом случае для распознавания слов в тексте, захваченном при оптическом распознавании символов, можно было бы использовать рекуррентную нейронную сеть (РНС). А что, если бы это можно было сделать на встроенном устройстве, более лёгком и компактном, чем даже смартфон?

Такое лёгкое, мощное TTS-устройство может помочь людям с нарушениями зрения. Его можно встроить в защищённые от неумелого обращения устройства для повышения грамотности или обработки рассказов и найти ему многие другие применения.

В этой статье я покажу, как это сделать с помощью TensorFlow, OpenCV, Festival и Raspberry Pi. Для оптического распознавания текста я буду использовать платформу машинного обучения TensorFlow вместе с предварительно обученной моделью Keras-OCR. Библиотека OpenCV будет использоваться для захвата изображений с веб-камеры. Наконец, в качестве TTS-модуля будет выступать система синтеза речи Festival. Затем всё соединим, чтобы создать приложение на Python для Raspberry Pi.

Попутно я расскажу, как работают типичные OCR-модели и как дополнительно оптимизировать решение с помощью TensorFlow Lite, набора инструментов для запуска оптимизированных моделей TensorFlow в ограниченных средах, таких как встраиваемые устройства и устройства Интернета вещей. Полный исходный код, представленный здесь, доступен на моей странице GitHub.

Начало работы


Во-первых, чтобы создать устройство и приложение для этого туториала, понадобится Raspberry Pi. Для этого примера подойдут версии 2, 3 или 4. Вы также можете использовать собственный компьютер для разработки (мы тестировали код для Python 3.7).

Необходимо установить два пакета: tensorflow (2.1.0) и keras_ocr (0.7.1). Вот несколько полезных ссылок:


OCR с помощью рекуррентных нейронных сетей


Здесь для распознавания текста на изображениях я использую пакет keras_ocr. Этот пакет основан на платформе TensorFlow и свёрточной нейронной сети, которая первоначально была опубликована в качестве примера OCR на веб-сайте Keras.

Архитектуру сети можно разделить на три важных этапа. На первом берут входное изображение, а затем извлекают элементы, используя несколько свёрточных слоёв. Эти слои разделяют входное изображение по горизонтали. Для каждой части эти слои определяют набор элементов столбцов изображения. Данная последовательность элементов столбцов используется на втором этапе рекуррентными слоями.

Рекуррентные нейронные сети (РНС) обычно состоят из слоёв долгой краткосрочной памяти (LTSM). Долгая краткосрочная память произвела революцию во многих применениях ИИ, включая распознавание речи, создание субтитров к изображениям и анализ временных рядов. OCR-модели используют РНС для создания так называемой матрицы вероятностей символов. Эта матрица определяет степень уверенности в том, что заданный символ находится в конкретной части входного изображения.

Таким образом, на последнем этапе эта матрица используется для декодирования текста на изображении. Обычно люди используют алгоритм классификации по рейтингу (Connectionist Temporal Classification, CTC). CTC стремится преобразовать матрицу в осмысленное слово или последовательность таких слов. Такое преобразование не тривиальная задача, так как в соседних частях изображения могут быть найдены одинаковые символы. Кроме того, некоторые входные части могут не содержать символов.

Хотя OCR-системы на основе РНС эффективны, пытаясь внедрить их в свои проекты, можно столкнуться с множеством проблем. В идеале необходимо выполнить обучение преобразованию, чтобы настроить модель в соответствие со своими данными. Затем модель преобразуется в формат TensorFlow Lite, чтобы оптимизировать для вывода на оконечное устройство. Такой подход оказался успешным в мобильных приложениях компьютерного зрения. Например, многие предварительно обученные сети MobileNet эффективно классифицируют изображения на мобильных устройствах и устройствах Интернета вещей.

Однако TensorFlow Lite представляет собой подмножество TensorFlow, и поэтому в настоящее время поддерживается не каждая операция. Эта несовместимость становится проблемой, когда необходимо выполнить оптическое распознавание символов, подобное тому, что включено в пакет keras-ocr на устройстве Интернета вещей. Список возможных решений предоставлен на официальном сайте TensorFlow.

В этой статье я покажу, как использовать модель TensorFlow, поскольку двунаправленные слои LSTM (используемые в keras-ocr) еще не поддерживаются в TensorFlow Lite.

Предварительно обученная OCR-модель


Для начала я написал тестовый скрипт (ocr.py), который показывает, как использовать модель нейронной сети из keras-ocr:

# Importsimport keras_ocrimport helpers # Prepare OCR recognizerrecognizer = keras_ocr.recognition.Recognizer() # Load images and their labelsdataset_folder = 'Dataset'image_file_filter = '*.jpg' images_with_labels = helpers.load_images_from_folder(dataset_folder, image_file_filter) # Perform OCR recognition on the input imagespredicted_labels = []for image_with_label in images_with_labels:predicted_labels.append(recognizer.recognize(image_with_label[0])) # Display resultsrows = 4cols = 2font_size = 14helpers.plot_results(images_with_labels, predicted_labels, rows, cols, font_size)

Этот скрипт создаёт экземпляр объекта Recognizer на основе модуля keras_ocr.recognition. Затем скрипт загружает изображения и их метки из прикреплённого набора тестовых данных (папка Dataset). Этот набор данных содержит восемь случайно выбранных изображений из набора синтетических слов (Synth90k). Затем скрипт запускает оптическое распознавание символов на каждом изображении этого набора данных, а затем отображает результаты прогнозирования.



Для загрузки изображений и их меток я использую функцию load_images_from_folder, которую я реализовал в модуле helpers. Этот метод предполагает два параметра: путь к папке с изображениями и фильтр. Здесь я предполагаю, что изображения находятся в подпапке Dataset, и я читаю все изображения в формате JPEG (с расширением имени файла .jpg).

В наборе данных Synth90k каждое имя файла изображения содержит метку изображения между символами подчёркивания. Например: 199_pulpiest_61190.jpg. Таким образом, чтобы получить метку изображения, функция load_images_from_folder разделяет имя файла по символу подчёркивания, а затем берёт первый элемент полученной коллекции строк. Также обратите внимание, что функция load_images_from_folder возвращает массив кортежей. Каждый элемент такого массива содержит изображение и соответствующую метку. По этой причине я передаю обработчику OCR только первый элемент этого кортежа.

Для распознавания я использую метод распознавания объекта Recognizer. Этот метод возвращает прогнозируемую метку, которую я сохраняю в коллекции predicted_labels.

Наконец, я передаю коллекцию прогнозируемых меток, изображений и исходных меток другой вспомогательной функции, plot_results, которая отображает изображения в прямоугольной сетке размера строки x столбцы. Внешний вид сетки можно изменить, изменив соответствующие переменные.

Камера


После тестирования OCR-модели я реализовал класс camera. В этом классе используется библиотека OpenCV, которая была установлена вместе с модулем keras-ocr. OpenCV предоставляет собой удобный программный интерфейс для доступа к камере. В явном виде вы сначала инициализируете объект VideoCapture, а затем вызываете его метод чтения (read), чтобы получить изображение с камеры.

import cv2 as opencv class camera(object):def __init__(self):# Initialize the camera captureself.camera_capture = opencv.VideoCapture(0)def capture_frame(self, ignore_first_frame):# Get frame, ignore the first one if neededif(ignore_first_frame):self.camera_capture.read()(capture_status, current_camera_frame) = self.camera_capture.read() # Verify capture statusif(capture_status):return current_camera_frame else:# Print error to the consoleprint('Capture error')

В этом коде я создал объект VideoCapture в инициализаторе класса camera. Я передаю объекту VideoCapture значение 0, чтобы указать на камеру системы по умолчанию. Затем я сохраняю полученный объект в поле camera_capture класса camera.

Чтобы получать изображения с камеры, я реализовал метод capture_frame. У него есть дополнительный параметр, ignore_first_frame. Когда значение этого параметра равно True, я дважды вызываю метод caper_capture.read, но игнорирую результат первого вызова. Смысл этой операции заключается в том, что первый кадр, возвращаемый моей камерой, обычно пуст.

Второй вызов метода read дает статус захвата и кадр. Если сбор данных был успешным (capture_status = True), я возвращаю кадр камеры. В противном случае я печатаю строку Ошибка захвата.

Преобразование текста в речь


Последний элемент данного приложения TTS-модуль. Было решено использовать здесь систему Festival, потому что она может работать в автономном режиме. Другие возможные подходы к TTS хорошо описаны в статье Adafruit Speech Synthesis on the Raspberry Pi (Синтез речи на Raspberry Pi).
Чтобы установить Festival на Raspberry Pi, выполните следующую команду:

sudo apt-get install festival -y

Убедиться в том, что всё работает правильно, можно, введя следующую команду:

echo "Hello, Arm" | Festival tts

Ваш Raspberry Pi должен произнести: Hello, Arm.
Festival предоставляет API-интерфейс. Однако для простоты было решено взаимодействовать с Festival посредством командной строки. С этой целью модуль helpers был дополнен ещё одним методом:

def say_text(text):os.system('echo ' + text + ' | festival --tts')

Собираем всё вместе


Наконец, мы можем собрать всё вместе. Я сделал это в скрипте main.py:

import keras_ocrimport camera as camimport helpers if __name__ == "__main__":# Prepare recognizerrecognizer = keras_ocr.recognition.Recognizer() # Get image from the cameracamera = cam.camera() # Ignore the first frame, which is typically blank on my machineimage = camera.capture_frame(True) # Perform recognitionlabel = recognizer.recognize(image) # Perform TTS (speak label)helpers.say_text('The recognition result is: ' + label)

Сначала я создаю OCR-распознаватель. Затем я создаю объект Camera и считываю кадр с веб-камеры по умолчанию. Изображение передаётся распознавателю, а полученная в результате метка произносится вспомогательным TTS-модулем.

Заключение


Итак, мы создали надёжную систему, которая способна оптически распознавать символы с использованием глубокого обучения, а затем передавать результаты пользователям посредством механизма преобразования текста в речь. Мы использовали пакет keras-OCR с предварительным обучением.

В более сложном сценарии распознаванию текста может предшествовать обнаружение текста. Сначала на изображении обнаруживаются строки текста, а затем распознаётся каждая из них. Для этого потребуются только возможности пакета keras-ocr по обнаружению текста. Это было показано в данной версии реализации Keras CRNN и опубликованной модели обнаружения текста CRAFT Фаусто Моралесом.

Расширяя указанное выше приложение функцией обнаружения текста, можно создать систему Интернета вещей, которая поддерживает РНС и выполняет OCR, чтобы помочь людям с нарушениями зрения читать меню в ресторанах или документы в государственных учреждениях. Более того, такое приложение с поддержкой службы переводов могло бы служить автоматическим переводчиком.

Хочется завершить этот материал цитатой третьего закона Артура Кларка:

Любая достаточно развитая технология неотличима от магии.

Если следовать ему то можно спокойно сказать, что у нас в SkillFactory мы обучаем людей настоящей магии, просто она называется data science и machine learning.



image
Подробнее..

Перевод Как распознать рукописный текст с помощью ИИ на микроконтроллерах

18.02.2021 18:15:13 | Автор: admin


Распознавание рукописных цифр с помощью TensorFlow и MNIST стало довольно распространённым введением в искусственный интеллект (ИИ) и ML. MNIST это база данных, которая содержит 70 000 примеров рукописных цифр. Она широко используется как источник изображений для обучения систем обработки изображений и программного обеспечения для машинного обучения.

Хотя учебные пособия по ML с использованием TensorFlow и MNIST стали привычными, до недавнего времени они обычно демонстрировались в полнофункциональных средах обработки с архитектурой x86 и графическими процессорами класса рабочих станций. Однако сегодня можно создать полнофункциональное приложение для распознавания рукописного ввода MNIST даже на 8-разрядном микроконтроллере. Чтобы продемонстрировать это, мы собираемся создать полнофункциональное приложение для распознавания рукописного ввода MNIST, используя TensorFlow Lite для получения результатов ИИ на маломощном микроконтроллере STMicroelectronics на базе процессора ARM Cortex M7.



В этой статье предполагается, что вы знакомы с C/C++ и машинным обучением, но не волнуйтесь, если это не так. Вы всё равно можете изучить предлагаемый пример и попробовать развернуть проект на собственном устройстве дома! Подобная реализация демонстрирует возможность создания надёжных приложений на основе машинного обучения даже на автономных устройствах с батарейным питанием в самых разных сценариях для Интернета вещей и портативных устройств.



Для создания этого проекта потребуется несколько компонентов:


Код этого проекта можно найти на GitHub.

Краткий обзор


Прежде чем начать, давайте рассмотрим действия, необходимые для запуска проекта ИИ с глубоким обучением на микроконтроллере с помощью TensorFlow:

  1. Обучить прогнозирующую модель на основе набора данных (рукописные цифры MNIST).
  2. Преобразовать модель в формат TensorFlow Lite.
  3. Создать встроенное приложение.
  4. Создать образцы данных.
  5. Развернуть и протестировать приложение.

Чтобы ускорить и упростить этот процесс, я создал записную книжку Jupyter в Google Colab, чтобы сделать первые два шага за вас из вашего браузера, не устанавливая и не настраивая Python на вашем компьютере. Она также может служить справочным материалом для других проектов, поскольку содержит весь код, необходимый для обучения и оценки модели MNIST с помощью TensorFlow, а также для преобразования модели в целях автономного использования в TensorFlow Lite для микроконтроллеров и создания версии кода массива Си модели для простой компиляции в любую программу на C++.

Чтобы перейти к встроенному приложению на шаге 3, сначала в меню записной книжки нажмите Runtime Run All (Время выполнения > Выполнить всё), чтобы создать файл model.h. Загрузите его из списка файлов на левой стороне. Также можно загрузить предварительно созданную модель из репозитория GitHub, чтобы включить её в проект.

Чтобы выполнить эти действия локально на своём компьютере, убедитесь, что используете платформу TensorFlow версии 2.0 или более поздней и дистрибутив Anaconda для установки и использования Python. Если вы используете упомянутую ранее записную книжку Jupyter, о которой говорилось выше, вам не придётся беспокоиться об установке TensorFlow 2.0, так как эта версия входит в состав этой записной книжки.

Обучение модели TensorFlow с использованием MNIST


Keras это высокоуровневая библиотека Python для нейронных сетей, часто используемая для создания прототипов ИИ-решений. Она интегрирована с TensorFlow, а также содержит встроенный набор данных MNIST из 60 000 изображений и 10 000 тестовых образцов, доступных прямо в TensorFlow.

Чтобы прогнозировать рукописные цифры, этот набор данных использовался для обучения относительно простой модели, в которой изображение 2828 принимается в качестве входной формы и выводятся до 10 категорий результатов с помощью функции активации Softmax с одним скрытым слоем между входным и выходным слоями. Этого было достаточно для достижения точности 96,6 %, но при желании можно добавить больше скрытых слоёв или тензоров.

За более глубоким обсуждением работы с набором данных MNIST в TensorFlow я рекомендую обратиться к некоторым (из многих) замечательным учебным пособиям по TensorFlow в Интернете, таким как Not another MNIST tutorial with TensorFlow, автор О'Рейли (O'Reilly). Вы также можете обратиться к примеру синусоидальной модели TensorFlow в этой записной книжке, чтобы ознакомиться с обучением и оценкой моделей TensorFlow и преобразованием модели в формат TensorFlow Lite для микроконтроллеров.



Преобразование модели в формат TensorFlow Lite


Созданная на первом шаге модель полезна и очень точна, но размер файла и использование памяти делают её недоступной для переноса или использования на встроенном устройстве. Именно здесь на помощь приходит TensorFlow Lite, так как данная среда выполнения оптимизирована для мобильных, встроенных устройств и устройств Интернета вещей и обеспечивает низкую задержку при очень небольших требованиях к размеру (всего несколько килобайт!). Это позволяет найти компромисс между точностью, скоростью и размером и выбрать модель в соответствии со своими потребностями.

В этом случае платформа TensorFlow Lite нужна, чтобы приложение занимало как можно меньше места во флеш-памяти и ОЗУ, оставаясь при этом быстрым, чтобы можно было немного понизить точность, не жертвуя слишком многим.

Чтобы ещё больше уменьшить размер, преобразователь TensorFlow Lite поддерживает дискретизацию модели, чтобы перейти в вычислениях от 32-разрядных значений с плавающей запятой к 8-разрядным целым числам, так как часто высокая точность значений с плавающей запятой не требуется. В результате также значительно уменьшается размер модели и повышается производительность.

Мне не удалось получить дискретизированную модель для правильного и согласованного использования функции Softmax. На моём устройстве STM32F7 Discovery возникает ошибка не удалось вызвать. Преобразователь TensorFlow Lite постоянно развивается, и некоторые конструкции моделей ещё не поддерживаются. Например, этот инструмент преобразует некоторые веса в значения типа int8 вместо uint8, а тип int8 не поддерживается. По крайней мере пока.

При этом, если преобразователь поддерживает все элементы, используемые в вашей модели, он может значительно уменьшить размер модели, и для этого требуется всего несколько строк кода. Поэтому я рекомендую попробовать. Необходимые строки кода просто закомментированы в моей записной книжке и готовы к тому, чтобы вы их раскомментировали и сгенерировали окончательную модель, чтобы проверить, правильно ли она работает на вашем устройстве.

У встроенных микроконтроллеров в полевых условиях часто ограничено пространство для хранения данных. На стенде для внешнего хранения данных всегда можно использовать карту памяти большего размера. Однако, чтобы смоделировать среду без доступа к внешнему хранилищу для файлов .tflite, можно экспортировать модель как код, чтобы она содержалась в самом приложении.

Я добавил скрипт Python в конец своей записной книжки, чтобы обработать эту часть и превратить её в файл model.h. При желании в Linux с помощью команды оболочки xxd -i созданный tflite-файл также можно преобразовать в массив Си. Загрузите этот файл из меню слева и приготовьтесь добавить его в проект встроенного приложения на следующем шаге.

import binasciidef convert_to_c_array(bytes) -> str:  hexstr = binascii.hexlify(bytes).decode("UTF-8")  hexstr = hexstr.upper()  array = ["0x" + hexstr[i:i + 2] for i in range(0, len(hexstr), 2)]  array = [array[i:i+10] for i in range(0, len(array), 10)]  return ",\n  ".join([", ".join(e) for e in array])tflite_binary = open("model.tflite", 'rb').read()ascii_bytes = convert_to_c_array(tflite_binary)c_file = "const unsigned char tf_model[] = {\n  " + ascii_bytes +   "\n};\nunsigned int tf_model_len = " + str(len(tflite_binary)) + ";"# print(c_file)open("model.h", "w").write(c_file)

Создание встроенного приложения


Теперь мы готовы взять нашу обученную модель MNIST и реализовать её на реальном маломощном микроконтроллере. Ваши конкретные действия могут зависеть от используемого набора инструментов, но с моими интегрированной средой разработки PlatformIO и устройством STM32F746G Discovery мною были предприняты следующие действия.

Сначала создан новый проект приложения с настройками для соответствующего устройства на базе ARM Cortex-M и подготовлены основные функции setup и loop. Я выбрал структуру Stm32Cube, чтобы выводить результаты на экран. Если вы используете Stm32Cube, вы можете загрузить файлы stm32_app.h и stm32_app.c из репозитория и создать файл main.cpp с функциями setup и loop, например, как здесь:

#include "stm32_app.h"void setup() {}void loop() {}



Добавьте или загрузите библиотеку TensorFlow Lite Micro. Я предварительно настроил библиотеку для интегрированной среды разработки PlateformIO, чтобы вы могли загрузить папку tfmicro отсюда в папку lib проекта и добавить её в качестве зависимости библиотеки в файл platformio.ini:

[env:disco_f746ng]platform = ststm32board = disco_f746ngframework = stm32cubelib_deps = tfmicro

В верхней части своего кода укажите заголовки библиотек TensorFlowLite, например, как здесь:

#include "stm32_app.h"#include "tensorflow/lite/experimental/micro/kernels/all_ops_resolver.h"#include "tensorflow/lite/experimental/micro/micro_error_reporter.h"#include "tensorflow/lite/experimental/micro/micro_interpreter.h"#include "tensorflow/lite/schema/schema_generated.h"#include "tensorflow/lite/version.h"void setup() {}void loop() {}

Включите преобразованный ранее файл model.h в этот проект в папку Include и добавьте его под заголовками TensorFlow. Затем сохраните результат и выполните сборку, чтобы убедиться, что всё в порядке, ошибок нет.

#include "model.h"



Определите для TensorFlow следующие глобальные переменные, которые будут использоваться в вашем коде:

// Globalsconst tflite::Model* model = nullptr;tflite::MicroInterpreter* interpreter = nullptr;tflite::ErrorReporter* reporter = nullptr;TfLiteTensor* input = nullptr;TfLiteTensor* output = nullptr;constexpr int kTensorArenaSize = 5000; // Just pick a big enough numberuint8_t tensor_arena[ kTensorArenaSize ] = { 0 };float* input_buffer = nullptr;

В своей функции setup загрузите модель, настройте средство выполнения TensorFlow, назначьте тензоры и сохраните входные и выходные векторы вместе с указателем на входной буфер, с которым мы будем взаимодействовать как с массивом значений с плавающей запятой. Теперь ваша функция должна выглядеть следующим образом:

void setup() {  // Load Model  static tflite::MicroErrorReporter error_reporter;  reporter = &error_reporter;  reporter->Report( "Let's use AI to recognize some numbers!" );  model = tflite::GetModel( tf_model );  if( model->version() != TFLITE_SCHEMA_VERSION ) {reporter->Report(   "Model is schema version: %d\nSupported schema version is: %d",   model->version(), TFLITE_SCHEMA_VERSION );return;  }   // Set up our TF runner  static tflite::ops::micro::AllOpsResolver resolver;  static tflite::MicroInterpreter static_interpreter(  model, resolver, tensor_arena, kTensorArenaSize, reporter );  interpreter = &static_interpreter;   // Allocate memory from the tensor_arena for the model's tensors.  TfLiteStatus allocate_status = interpreter->AllocateTensors();  if( allocate_status != kTfLiteOk ) {reporter->Report( "AllocateTensors() failed" );return;  }  // Obtain pointers to the model's input and output tensors.  input = interpreter->input(0);  output = interpreter->output(0);  // Save the input buffer to put our MNIST images into  input_buffer = input->data.f;}

Подготовьте TensorFlow к выполнению на устройстве ARM Cortex-M при каждом вызове функции loop с короткой задержкой (одна секунда) между обновлениями, например, как здесь:

void loop() {  // Run our model  TfLiteStatus invoke_status = interpreter->Invoke();  if( invoke_status != kTfLiteOk ) {reporter->Report( "Invoke failed" );return;  }   float* result = output->data.f;  char resultText[ 256 ];  sprintf( resultText, "It looks like the number: %d", std::distance( result, std::max_element( result, result + 10 ) ) );  draw_text( resultText, 0xFF0000FF );  // Wait 1-sec til before running again  delay( 1000 );}

Приложение готово к работе. Оно просто ждёт, когда мы скормим ему несколько забавных тестовых изображений MNIST для обработки!

Создание образца данных MNIST для встраивания


Теперь давайте получим несколько изображений рукописных цифр, которые наше устройство сможет прочитать.

Чтобы добавить эти изображения в программу независимо от внешнего хранилища, мы можем заранее преобразовать 100 изображений MNIST из формата JPEG в чёрно-белые изображения, сохранённые в виде массивов, так же как и наша модель TensorFlow. Для этого я использовал веб-инструмент с открытым исходным кодом под названием image2cpp, который выполняет большую часть этой работы за нас в одном пакете. Если вы хотите сгенерировать их самостоятельно, проанализируйте пиксели и закодируйте по восемь в каждый байт и запишите их в формате массива Си, как показано ниже.

ПРИМЕЧАНИЕ. Веб-инструмент генерирует код для интегрированной среды разработки Arduino, поэтому в коде найдите и удалите все экземпляры PROGMEM, а затем компилируйте код в среде PlatformIO.

Например, это тестовое изображение рукописного нуля должно быть преобразовано в следующий массив:



// 'mnist_0_1', 28x28pxconst unsigned char mnist_1 [] PROGMEM = {  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00,  0x00, 0x3f, 0xe0, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x7e, 0x30, 0x00, 0x00, 0xfc, 0x38, 0x00,  0x00, 0xf0, 0x1c, 0x00, 0x00, 0xe0, 0x1c, 0x00, 0x00, 0xc0, 0x1e, 0x00, 0x00, 0xc0, 0x1c, 0x00,  0x01, 0xc0, 0x3c, 0x00, 0x01, 0xc0, 0xf8, 0x00, 0x01, 0xc1, 0xf8, 0x00, 0x01, 0xcf, 0xf0, 0x00,  0x00, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

Сохраните сгенерированные изображения в новый файл mnist.h в проекте или, чтобы сэкономить время и пропустить этот шаг, можно просто загрузить мою версию изGitHub.

В нижней части файла я объединил все массивы в одну окончательную коллекцию, чтобы каждую секунду мы могли выбрать для обработки произвольное изображение:

const unsigned char* test_images[] = {  mnist_1, mnist_2, mnist_3, mnist_4, mnist_5,   mnist_6, mnist_7, mnist_8, mnist_9, mnist_10,  mnist_11, mnist_12, mnist_13, mnist_14, mnist_15,   mnist_16, mnist_17, mnist_18, mnist_19, mnist_20,  mnist_21, mnist_22, mnist_23, mnist_24, mnist_25,   mnist_26, mnist_27, mnist_28, mnist_29, mnist_30,  mnist_31, mnist_32, mnist_33, mnist_34, mnist_35,   mnist_36, mnist_37, mnist_38, mnist_39, mnist_40,  mnist_41, mnist_42, mnist_43, mnist_44, mnist_45,   mnist_46, mnist_47, mnist_48, mnist_49, mnist_50,  mnist_51, mnist_52, mnist_53, mnist_54, mnist_55,   mnist_56, mnist_57, mnist_58, mnist_59, mnist_60,  mnist_61, mnist_62, mnist_63, mnist_64, mnist_65,   mnist_66, mnist_67, mnist_68, mnist_69, mnist_70,  mnist_71, mnist_72, mnist_73, mnist_74, mnist_75,   mnist_76, mnist_77, mnist_78, mnist_79, mnist_80,  mnist_81, mnist_82, mnist_83, mnist_84, mnist_85,   mnist_86, mnist_87, mnist_88, mnist_89, mnist_90,  mnist_91, mnist_92, mnist_93, mnist_94, mnist_95,   mnist_96, mnist_97, mnist_98, mnist_99, mnist_100,};

Не забудьте включить в верхнюю часть кода заголовок нового изображения:

#include "mnist.h"

Тестирование изображений MNIST


После добавления этих образцов изображений в код можно добавить две вспомогательные функции: одна для считывания монохромного изображения во входной вектор, а другая для визуализации на встроенном дисплее. Ниже перечислены функции, которые я разместил непосредственно над функцией setup:

void bitmap_to_float_array( float* dest, const unsigned char* bitmap ) { // Populate input_vec with the monochrome 1bpp bitmap  int pixel = 0;  for( int y = 0; y < 28; y++ ) {for( int x = 0; x < 28; x++ ) {  int B = x / 8; // the Byte # of the row  int b = x % 8; // the Bit # of the Byte  dest[ pixel ] = ( bitmap[ y * 4 + B ] >> ( 7 - b ) ) & 0x1 ? 1.0f : 0.0f;  pixel++;}  }}void draw_input_buffer() {  clear_display();  for( int y = 0; y < 28; y++ ) {for( int x = 0; x < 28; x++ ) {  draw_pixel( x + 16, y + 3, input_buffer[ y * 28 + x ] > 0 ? 0xFFFFFFFF : 0xFF000000 );}  }}

И, наконец, в нашем цикле можно выбрать случайное тестовое изображение для считывания во входной буфер и рисования на дисплее, например, как здесь:

void loop() {  // Pick a random test image for input  const int num_test_images = ( sizeof( test_images ) / sizeof( *test_images ) );  bitmap_to_float_array( input_buffer,  test_images[ rand() % num_test_images ] );  draw_input_buffer();   // Run our model  ...}

Если всё в порядке, ваш проект будет скомпонован и развёрнут, и вы увидите, как ваш микроконтроллер распознаёт все рукописные цифры и выдаёт отличные результаты! Верите?




Что дальше?


Теперь, когда вы узнали о возможностях маломощных микроконтроллеров ARM Cortex-M, позволяющих использовать возможности глубокого обучения с помощью TensorFlow, вы готовы сделать гораздо больше! От обнаружения животных и предметов различных типов до обучения устройства понимать речь или отвечать на вопросы вы со своим устройством можете открыть новые горизонты, которые ранее считались возможными только при использовании мощных компьютеров и устройств.

На GitHub доступны несколько потрясающих примеров TensorFlow Lite для микроконтроллеров, разработанных командой TensorFlow. Ознакомьтесь с этимирекомендациями, чтобы убедиться, что вы максимально эффективно используете свой проект ИИ, работающий на устройстве Arm Cortex-M. А если хотите прокачать себя в Machine Learning, Data Science или поднять уровень уже имеющихся знаний приходите учиться, будет сложно, но интересно.



image
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR:

Подробнее..

KotlinDL 0.2 Functional API, зоопарк моделей c ResNet и MobileNet, DSL для обработки изображений

25.05.2021 12:05:50 | Автор: admin

Представляем вам версию 0.2 библиотеки глубокого обучения KotlinDL.

KotlinDL 0.2 теперь доступен на Maven Central (до этого он лежал на bintray, но закатилось солнышко земли опенсорсной). Появилось столько всего нового: новые слои, специальный DSL для препроцессинга изображений, новые типы датасетов, зоопарк моделей с несколькими моделями из семейства ResNet, MobileNet и старой доброй моделью VGG (рабочая лошадка, впрочем).

В этой статье мы коснемся самых главных изменений релиза 0.2. Полный список изменений доступен по ссылке.

Functional API

Прошлая версия библиотеки позволяла описывать нейронные сети лишь при помощи Sequential API. Например, используя метод Sequential.of(..), вы могли легко описать модель как последовательность слоев и построить VGG-подобную модель.

Однако с 2014 года (эпохи взлета и расцвета подобных архитектур) много воды утекло, и было создано множество новых нейросетей. В частности, стандартным подходом стало использование так называемых остаточных нейросетей (Residual Neural Networks или ResNet), которые решают проблемы исчезающих градиентов (vanishing gradients) и, напротив, взрывающихся градиентов (exploding gradients) а значит, и проблемы деградации обучения нейросети. Подобные архитектуры невозможно описать в виде Sequential API их корректнее представлять в виде направленного ациклического графа (Directed Acyclic Graph). Для задания таких графов мы добавили в версии 0.2 новый Functional API, который позволяет нам описывать модели, подобные ResNet или MobileNet.

Ну что же, давайте построим некое подобие ResNet. Нейросеть будет обучаться на датасете FashionMnist (небольшие изображения модных вещей). Черно-белые изображения размером 28х28 отлично подойдут на старте работы с нейросетями.

val (train, test) = fashionMnist()val inputs = Input(28, 28, 1)val conv1 = Conv2D(32)(inputs)val conv2 = Conv2D(64)(conv1)val maxPool = MaxPool2D(poolSize = intArrayOf(1, 3, 3, 1),strides = intArrayOf(1, 3, 3, 1))(conv2)val conv3 = Conv2D(64)(maxPool)val conv4 = Conv2D(64)(conv3)val add1 = Add()(conv4, maxPool)val conv5 = Conv2D(64)(add1)val conv6 = Conv2D(64)(conv5)val add2 = Add()(conv6, add1)val conv7 = Conv2D(64)(add2)val globalAvgPool2D = GlobalAvgPool2D()(conv7)val dense1 = Dense(256)(globalAvgPool2D)val outputs = Dense(10, activation = Activations.Linear)(dense1)val model = Functional.fromOutput(outputs)model.use {it.compile(optimizer = Adam(),loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,metric = Metrics.ACCURACY)it.summary()it.fit(dataset = train, epochs = 3, batchSize = 1000)val accuracy = it.evaluate(dataset = test, batchSize = 1000).metrics[Metrics.ACCURACY]println("Accuracy after: $accuracy")}

Перед вами вывод метода summary(), описывающий архитектуру только что созданной нами модели.

Некоторые не любят сухие отчеты и предпочитают диаграммы. В нашем случае диаграмма типична для всех представителей славного семейства ResNet.

Если вы знакомы с фреймворком Keras, то без особого труда сможете перенести модели, описанные при помощи Functional API, в Keras, используя KotlinDL.

Коллекция предварительно тренированных моделей ResNet и MobileNet

Начиная с релиза 0.2, в Kotlin DL появляется зоопарк моделей (или Model Zoo). По сути, это коллекция моделей с весами, полученными в ходе обучения на большом датасете изображений (ImageNet).

Зачем нужна такая коллекция моделей? Дело в том, что современные сверхточные нейросети могут иметь сотни слоев и миллионы параметров, обновляемых многократно в течении каждой итерации обучения. Тренировка моделей до приемлемого уровня точности (7080%) на таком большом датасете, как ImageNet, может занимать сотни и тысячи часов вычислительного времени большого кластера из видеокарт.

Зоопарк моделей позволяет вам пользоваться уже готовыми и натренированными моделями (вам не придется тренировать их с нуля каждый раз, когда они вам нужны). Вы можете использовать такую модель непосредственно для предсказаний. Также вы можете применить ее для дотренировки части модели на небольшой порции входных данных это весьма распространненная техника при использовании переноса обучения (Transfer Learning). Это может занять десятки минут на одной видеокарте (или даже центральном процессоре) вместо сотен часов на большом кластере.

Доступны следующие модели:

  • VGG16

  • VGG19

  • ResNet50

  • ResNet101

  • ResNet152

  • ResNet50v2

  • ResNet101v2

  • ResNet152v2

  • MobileNet

  • MobileNetv2

Для каждой модели из этого списка доступны функции загрузки конфигурации модели в JSON-формате и весов в формате .h5. Также для каждой модели можно использовать специальный препроцессинг, применявшийся для ее обучения на датасете ImageNet.

Ниже вы видите пример загрузки одной из таких моделей (ResNet50):

// specify the model type to be loaded, ResNet50, for exampleval loader =ModelZoo(commonModelDirectory = File("cache/pretrainedModels"), modelType = ModelType.ResNet_50)// obtain the model configurationval model = loader.loadModel() as Functional// load class labels (from ImageNet dataset in ResNet50 case)val imageNetClassLabels = loader.loadClassLabels()// load weights if required (for Transfer Learning purposes)val hdfFile = loader.loadWeights()

Ну что же, теперь у вас есть сама модель и веса вы можете использовать их по вашему усмотрению.

Внимание! К изображениям, которые вы подаете на вход модели для предсказаний, необходимо применять специальный препроцессинг, о котором мы говорили ранее. Иначе вы получите неверные результаты. Для вызова препроцессинга используйте функцию preprocessInput.

Если вам не нужны предобученные веса, но вы не хотите описывать многослойные модели а-ля VGG или ResNet с нуля, у вас есть два пути: а) просто загрузить конфигурацию модели либо б) взять за основу полный код конструирования модели, написанный на Kotlin, он доступен для каждой из моделей через вызов функции высшего порядка, лежащей в пакете org.jetbrains.kotlinx.dl.api.core.model.

Ниже мы приводим пример использования функции, строящей облегченную версию ResNet50:

val model = resnet50Light(imageSize = 28,numberOfClasses = 10,numberOfChannels = 1,lastLayerActivation = Activations.Linear)

Если вы хотите узнать больше о переносе обучения и использовании зоопарка моделей, советуем этот туториал: вы увидите, как загружается модель VGG19, затем у нее удаляется последний слой, добавляются новые Dense-слои, после чего их веса инициализируются и дообучаются на небольшом датасете, состоящем из изображений кошек и собак.

DSL для предобработки изображений

Python-разработчикам предлагается огромное количество библиотек визуализации и предобработки изображений, музыки и видео. Разработчикам экосистемы языков программирования JVM повезло меньше.

Большинство библиотек для предобработки изображений, найденные на просторах Github и имеющие разную степень заброшенности, так или иначе используют класс BufferedImage, оборачивая его более понятным и согласованным API. Мы решили упростить жизнь Kotlin-разработчиков, предложив им простой DSL, построенный на лямбда-выражениях и объектах-приемниках.

На данный момент доступны следующие функции преобразования изображений:

  • Load

  • Crop

  • Resize

  • Rotate

  • Rescale

  • Sharpen

  • Save

val preprocessing: Preprocessing = preprocess {   transformImage {       load {           pathToData = imageDirectory           imageShape = ImageShape(224, 224, 3)           colorMode = ColorOrder.BGR       }       rotate {           degrees = 30f       }       crop {           left = 12           right = 12           top = 12           bottom = 12       }       resize {           outputWidth = 400           outputHeight = 400           interpolation = InterpolationType.NEAREST       }   }   transformTensor {       rescale {           scalingCoefficient = 255f       }   }}

Весьма популярной техникой при тренировке глубоких сверточных нейросетей является аугментация данных методика создания дополнительных обучающих данных из имеющихся данных. При помощи перечисленных функций можно организовать простейшую аугментацию: достаточно выполнять повороты изображения некоторый угол и менять его размеры.

Если, экспериментируя с DSL, вы поймете, что некоторых функций вам не хватает, не стесняйтесь написать об этом в наш баг-трекер.

Новые слои

В релизе 0.2 появилось много новых слоев. В основном, это обусловлено тем, что они используются в архитектурах ResNet и MobileNet:

  • BatchNorm

  • ActivationLayer

  • DepthwiseConv2D

  • SeparableConv2D

  • Merge (Add, Subtract, Multiply, Average, Concatenate, Maximum, Minimum)

  • GlobalAvgPool2D

  • Cropping2D

  • Reshape

  • ZeroPadding2D*

* Спасибо Anton Kosyakov за имплементацию нетривиального ZeroPadding2D!

Кстати, если вы хотите добавить новый слой, вы можете самостоятельно реализовать его и создать пул-реквест. Список слоев, которые мы хотели бы включить в релиз 0.3, представлен набором тикетов в баг-трекере с пометкой good first issue и может быть использован вами как точка входа в проект.

Dataset API и парочка наследников: OnHeapDataset & OnFlyDataset

Типичным способом прогона данных через нейросеть в режиме прямого распространения (forward mode) является последовательная загрузка батчей в оперативную память, контролируемую языком, а затем в область нативной памяти, контролируемую вычислительным графом модели TensorFlow.

Мы также поддерживаем подобный подход в OnFlyDataset. Он последовательно, батч за батчем, загружает датасет в течений одной тренировочной эпохи, применяя препроцессинг данных (если вы его заранее определили) и аугментацию (если вы ее добавили).

Этот метод хорош, когда оперативной памяти мало, а данных много. Но что, если оперативной памяти более чем достаточно? Это не такой уж редкий случай для задач переноса обучения: датасеты для дообучения могут быть не такими большими, как при тренировке моделей. Также можно получить некоторый прирост в скорости за счет того, что препроцессинг будет применен лишь один раз на этапе формирования датасета, а не при каждой загрузке батча. Если у вас достаточно оперативной памяти, используйте OnHeapDataset. Он будет держать все данные в оперативной памяти не нужно будет повторно считывать их с диска на каждой эпохе.

Набор встроенных датасетов

Если вы только начинаете путешествие в удивительный мир глубокого обучения, мы настоятельно рекомендуем вам строить и запускать ваши первые нейросети на широко известных датасетах, таких как MNIST (набор рукописных цифр), FashionMNIST(набор изображений модных вещей от компании Zalando), Cifar10 (подмножество ImageNet, насчитывающее 50 000 изображений) или коллекцию изображений кошек и собак со знаменитого соревнования Kaggle (по 25 000 изображений каждого класса различных размеров).

Все эти датасеты, как и модели из зоопарка моделей, вы можете загрузить в папку на вашем диске при помощи функций высшего порядка, таких как mnist() и fashionMnist(). Если датасет уже был загружен, заново по сети он грузиться не будет, а будет взят с диска.

Как добавить KotlinDL в проект

Чтобы начать использовать KotlinDL в вашем проекте, просто добавьте дополнительную зависимость в файл build.gradle:

repositories {    mavenCentral()}dependencies {    implementation 'org.jetbrains.kotlinx:kotlin-deeplearning-api:0.2.0'}

KotlinDL можно использовать в Java-проектах, даже если у вас нет ни капли Kotlin-кода. Здесь вы найдете пример построения и тренировки сверточной сети, полностью написанный на Java.

Если вы думаете, что в вашем проекте будет полезен Java API, напишите нам об этом или создайте PR.

Полезные ссылки

Мы надеемся, что вам понравилась наша статья и новые возможности KotlinDL.

Хотите узнать больше о проекте? Предлагаем ознакомиться с Readme или со страничкой проекта на GitHub. А этот туториал поможет вам создать вашу первую нейросеть на Kotlin.

Если вам интересно, как устроен KotlinDL, как он появился и в каком направлении развивается, почему он так похож на Keras, и планируется ли поддержка PyTorch, посмотрите свежее видео от Алексея Зиновьева.

Также мы ждем вас в Slack-канале #kotlindl (инвайт можно получить тут). В нем вы можете задавать вопросы, участвовать в дискуссиях и первыми получать информацию о превью-релизах и новых моделях в зоопарке моделей.

Ваша обратная связь, ваши описания багов и краш-репорты, идеи и комментарии все это очень важно для нас. Мы ждем новых пользователей и контрибьюторов, как начинающих, так и опытных исследователей всех, кому интересны Deep Learning и Data Science на Kotlin, Java и Scala!

Подробнее..

Обнаружение объектов с помощью YOLOv3 на Tensorflow 2.0

08.05.2021 14:13:54 | Автор: admin
Кадр из аниме "Жрица и медведь"Кадр из аниме "Жрица и медведь"

До появления YOLO большинство способов обнаружения объектов пытались адаптировать классификаторы для детекции. В YOLO же, обнаружение объектов было сформулировано как задача регрессии на пространственно разделенных ограничивающих рамок (bounding boxes) и связанных с ними вероятностей классов.

В данной статье мы узнаем о системе YOLO Object Detection и как реализовать подобную систему в Tensorflow 2.0

О YOLO:

Наша унифицированная архитектура чрезвычайно быстра. Базовая модель YOLO обрабатывает изображения в режиме реального времени со скоростью 45 кадров в секунду. Уменьшенная версия сети, Fast YOLO, обрабатывает аж 155 кадра в секунду

You Only Look Once: Unified, Real-Time Object Detection, 2015

Что такое YOLO?

YOLO это новейшая (на момент написания оригинальной статьи) система (сеть) обнаружения объектов. Она была разработана Джозефом Редмоном (Joseph Redmon). Наибольшим преимуществом YOLO над другими архитектурами является скорость. Модели семейства YOLO исключительно быстры и намного превосходят R-CNN (Region-Based Convolutional Neural Network) и другие модели. Это позволяет добиться обнаружения объектов в режиме реального времени.

На момент первой публикации (в 2016 году) по сравнению с другими системами, такими как R-CNN и DPM (Deformable Part Model), YOLO добилась передового значения mAP (mean Average Precision). С другой стороны, YOLO испытывает трудности с точной локализацией объектов. Однако в новой версии были внесены улучшения в скорости и точности системы.

Альтернативы (на момент публикации статьи): Другие архитектуры в основном использовали метод скользящего окна по всему изображению, и классификатор использовался для определенной области изображения (DPM). Также, R-CNN использовал метод предложения регионов (region proposal method). Описываемый метод сначала создает потенциальные bounding boxы. Затем, на области, ограниченные bounding boxами, запускается классификатор и следующее удаление повторяющихся распознаваний, и уточнение границ рамок.

YOLO переосмыслила задачу обнаружения объектов в задачу регрессии.Она идет от пикселей изображения к координатам bounding boxов и вероятностей классов. Тем самым, единая сверточная сеть предсказывает несколько bounding boxов и вероятности классов для содержания этих областей.

Теория

Так как YOLO необходимо только один взгляд на изображение, то метод скользящего окна не подходит в данной ситуации. Вместо этого, изображение будет поделено на сетку с ячейками размером S x S. Каждая ячейка может содержать несколько разных объектов для распознавания.

Во-первых, каждая ячейка отвечает за прогнозирование количества bounding boxов. Также, каждая ячейка прогнозирует доверительное значение (confidence value) для каждой области, ограниченной bounding boxом. Иными словами, это значение определяет вероятность нахождения того или иного объекта в данной области. То есть в случае, если какая-то ячейка сетки не имеет определенного объекта, важно, чтобы доверительное значение для этой области было низким.

Когда мы визуализируем все предсказания, мы получаем карту объектов и упорядоченных по доверительному значению, рамки.

Во-вторых, каждая ячейка отвечает за предсказание вероятностей классов. Это не говорит о том, что какая-то ячейка содержит какой-то объект, только вероятность нахождения объекта. Допустим, если ячейка предсказывает автомобиль, это не гарантирует, что автомобиль в действительности присутствует в ней. Это говорит лишь о том, что если присутствует объект, то этот объект скорее всего автомобиль.

Давайте подробней опишем вывод модели.

В YOLO используются anchor boxes (якорные рамки / фиксированные рамки) для прогнозирования bounding boxов. Идея anchor boxов сводится к предварительному определению двух различных форм. И таким образом, мы можем объединить два предсказания с двумя anchor boxами (в целом, мы могли бы использовать даже большее количество anchor boxов). Эти якоря были рассчитаны с помощью датасета COCO (Common Objects in Context) и кластеризации k-средних (K-means clustering).

У нас есть сетка, где каждая ячейка предсказывает:

  • Для каждого bounding box'а:

    • 4 координаты (tx , ty , tw , th)

    • 1 objectness error (ошибка объектности), которая является показателем уверенности в присутствии того или иного объекта

  • Некоторое количество вероятностей классов

Если же присутствует некоторое смещение от верхнего левого угла на cx , cy то прогнозы будут соответствовать:

b_{x} = \sigma(t_{x}) + c_{x}\\ b_{y} = \sigma(t_{y}) + c_{y}\\ b_{w} = p_{w}e^{t_{w}}\\ b_{h} = p_{h}e^{t_{h}}

где pw (ширина) и ph (высота) соответствуют ширине и высоте bounding box'а. Вместо того, чтобы предугадывать смещение как в прошлой версии YOLOv2, авторы прогнозируют координаты местоположения относительно местоположения ячейки.

Этот вывод является выводом нашей нейронной сети. В общей сложности здесьS x S x [B * (4+1+C)] выводов, где B это количество bounding box'ов, которое может предсказать ячейка на карте объектов, C это количество классов, 4 для bounding box'ов, 1 для objectness prediction (прогнозирование объектности). За один проход мы можем пройти от входного изображения к выходному тензору, который соответствует обнаруженным объектам на картинке. Также стоит отметить, что YOLOv3 прогнозирует bounding box'ы в трех разных масштабах.

Теперь, если мы возьмем вероятность и умножим их на доверительные значения, мы получим все bounding box'ы, взвешенные по вероятности содержания этого объекта.

Простое нахождение порогового значения избавит нас от прогнозов с низким доверительным значением. Для следующего шага важно определить метрику IoU (Intersection over Union / Пересечение над объединением). Эта метрика равняется соотношению площади пересекающихся областей к площади областей объединенных.

После этого все равно могут остаться дубликаты, и чтобы от них избавиться нужно использовать подавление не-максимумов (non-maximum suppression). Подавление не-максимумов заключается в следующем: алгоритм берёт bounding box с наибольшей вероятностью принадлежности к объекту, затем, среди остальных граничащих bounding box'ов с данной области, возьмёт один с наивысшим IoU и подавляет его.

Ввиду того, что все делается за один прогон, эта модель будет работать почти также быстро, как и классификация. К тому же все обнаружения предсказываются одновременно, что означает, что модель неявно учитывает глобальный контекст. Проще говоря, модель может узнать какие объекты обычно встречаться вместе, их относительный размер и расположение объектов и так далее.

Yolov3Yolov3

Мы также рекомендуем прочитать следующие статьи о YOLO:

Реализация в Tensorflow

Первым шагом в реализации YOLO это подготовка ноутбука и импортирование необходимых библиотек. Целиком ноутбук с кодом вы можете на Github или Kaggle:

Следуя этой статье, мы сделаем полную сверточную сеть (fully convolutional network / FCN) без обучения. Для того, чтобы применить эту сеть для определения объектов, нам необходимо скачать готовые веса от предварительно обученной модели. Эти веса были получены от обучения YOLOv3 на датасете COCO (Common Objects in Context). Файл с весами можно скачать по ссылке официального сайта.

# Создаем папку для checkpoint'ов с весами.# !mkdir checkpoints# Скачиваем файл с весами для YOLOv3 с официального сайта.# !wget https://pjreddie.com/media/files/yolov3.weights# Импортируем необходимые библиотеки.import cv2import numpy as np import tensorflow as tf from absl import loggingfrom itertools import repeatfrom PIL import Imagefrom tensorflow.keras import Modelfrom tensorflow.keras.layers import Add, Concatenate, Lambdafrom tensorflow.keras.layers import Conv2D, Input, LeakyReLUfrom tensorflow.keras.layers import MaxPool2D, UpSampling2D, ZeroPadding2Dfrom tensorflow.keras.regularizers import l2from tensorflow.keras.losses import binary_crossentropyfrom tensorflow.keras.losses import sparse_categorical_crossentropyyolo_iou_threshold = 0.6 # Intersection Over Union (iou) threshold.yolo_score_threshold = 0.6 # Score threshold.weightyolov3 = 'yolov3.weights' # Путь до файла с весами.size = 416 # Размер изображения. checkpoints = 'checkpoints/yolov3.tf' # Путь до файла с checkpoint'ом.num_classes = 80 # Количество классов в модели.# Список слоев в YOLOv3 Fully Convolutional Network (FCN).YOLO_V3_LAYERS = [    'yolo_darknet',    'yolo_conv_0',    'yolo_output_0',    'yolo_conv_1',    'yolo_output_1',    'yolo_conv_2',    'yolo_output_2']

По причине того, что порядок слоев в Darknet (open source NN framework) и tf.keras разные, то загрузить веса с помощью чистого функционального API будет проблематично. В этом случае, наилучшим решением будет создание подмоделей в keras. TF Checkpoints рекомендованы для сохранения вложенных подмоделей и они официально поддерживаются Tensorflow.

# Функция для загрузки весов обученной модели.def load_darknet_weights(model, weights_file):    wf = open(weights_file, 'rb')    major, minor, revision, seen, _ = np.fromfile(wf, dtype=np.int32, count=5)    layers = YOLO_V3_LAYERS    for layer_name in layers:        sub_model = model.get_layer(layer_name)        for i, layer in enumerate(sub_model.layers):            if not layer.name.startswith('conv2d'):                continue            batch_norm = None            if i + 1 < len(sub_model.layers) and \                sub_model.layers[i + 1].name.startswith('batch_norm'):                    batch_norm = sub_model.layers[i + 1]            logging.info("{}/{} {}".format(                sub_model.name, layer.name, 'bn' if batch_norm else 'bias'))                        filters = layer.filters            size = layer.kernel_size[0]            in_dim = layer.input_shape[-1]            if batch_norm is None:                conv_bias = np.fromfile(wf, dtype=np.float32, count=filters)            else:                bn_weights = np.fromfile(wf, dtype=np.float32, count=4*filters)                bn_weights = bn_weights.reshape((4, filters))[[1, 0, 2, 3]]            conv_shape = (filters, in_dim, size, size)            conv_weights = np.fromfile(wf, dtype=np.float32, count=np.product(conv_shape))            conv_weights = conv_weights.reshape(conv_shape).transpose([2, 3, 1, 0])            if batch_norm is None:                layer.set_weights([conv_weights, conv_bias])            else:                layer.set_weights([conv_weights])                batch_norm.set_weights(bn_weights)    assert len(wf.read()) == 0, 'failed to read weights'    wf.close()

На этом же этапе, мы должны определить функцию для расчета IoU. Мы используем batch normalization (пакетная нормализация) для нормализации результатов, чтобы ускорить обучение. Так как tf.keras.layers.BatchNormalization работает не очень хорошо для трансферного обучения (transfer learning), то мы используем другой подход.

# Функция для расчета IoU.def interval_overlap(interval_1, interval_2):    x1, x2 = interval_1    x3, x4 = interval_2    if x3 < x1:        return 0 if x4 < x1 else (min(x2,x4) - x1)    else:        return 0 if x2 < x3 else (min(x2,x4) - x3)def intersectionOverUnion(box1, box2):    intersect_w = interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax])    intersect_h = interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax])    intersect_area = intersect_w * intersect_h    w1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin    w2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin    union_area = w1*h1 + w2*h2 - intersect_area    return float(intersect_area) / union_area class BatchNormalization(tf.keras.layers.BatchNormalization):    def call(self, x, training=False):        if training is None: training = tf.constant(False)        training = tf.logical_and(training, self.trainable)        return super().call(x, training)# Определяем 3 anchor box'а для каждой ячейки.   yolo_anchors = np.array([(10, 13), (16, 30), (33, 23), (30, 61), (62, 45),                        (59, 119), (116, 90), (156, 198), (373, 326)], np.float32) / 416yolo_anchor_masks = np.array([[6, 7, 8], [3, 4, 5], [0, 1, 2]])

В каждом масштабе мы определяем 3 anchor box'а для каждой ячейки. В нашем случае если маска будет:

  • 0, 1, 2 означает, что будут использованы первые три якорные рамки

  • 3, 4 ,5 означает, что будут использованы четвертая, пятая и шестая

  • 6, 7, 8 означает, что будут использованы седьмая, восьмая, девятая

# Функция для отрисовки bounding box'ов.def draw_outputs(img, outputs, class_names, white_list=None):    boxes, score, classes, nums = outputs    boxes, score, classes, nums = boxes[0], score[0], classes[0], nums[0]    wh = np.flip(img.shape[0:2])    for i in range(nums):        if class_names[int(classes[i])] not in white_list:            continue        x1y1 = tuple((np.array(boxes[i][0:2]) * wh).astype(np.int32))        x2y2 = tuple((np.array(boxes[i][2:4]) * wh).astype(np.int32))        img = cv2.rectangle(img, x1y1, x2y2, (255, 0, 0), 2)        img = cv2.putText(img, '{} {:.4f}'.format(            class_names[int(classes[i])], score[i]),            x1y1, cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 255), 2)    return img

Теперь пришло время для реализации YOLOv3. Идея заключается в том, чтобы использовать только сверточные слои. Так как их здесь 53, то самым простым способом является создание функции, в которую мы будем передавать важные параметры, меняющиеся от слоя к слою.

Остаточные блоки (Residual Blocks) в диаграмме архитектуры YOLOv3 применяются для изучения признаков. Остаточный блок содержит в себе несколько сверточных слоев и дополнительные связи для обхода этих слоев.

Создавая нашу модель, мы строим нашу модель с помощью функционального API, который будет легко использовать. С его помощью мы можем без труда определить ветви в нашей архитектуре (ResNet Block) и делить слои внутри архитектуры.

def DarknetConv(x, filters, size, strides=1, batch_norm=True):    if strides == 1:        padding = 'same'    else:        x = ZeroPadding2D(((1, 0), (1, 0)))(x)        padding = 'valid'    x = Conv2D(filters=filters, kernel_size=size,              strides=strides, padding=padding,              use_bias=not batch_norm, kernel_regularizer=l2(0.0005))(x)    if batch_norm:        x = BatchNormalization()(x)        x = LeakyReLU(alpha=0.1)(x)    return xdef DarknetResidual(x, filters):    previous = x    x = DarknetConv(x, filters // 2, 1)    x = DarknetConv(x, filters, 3)    x = Add()([previous , x])    return xdef DarknetBlock(x, filters, blocks):    x = DarknetConv(x, filters, 3, strides=2)    for _ in repeat(None, blocks):        x = DarknetResidual(x, filters)           return xdef Darknet(name=None):    x = inputs = Input([None, None, 3])    x = DarknetConv(x, 32, 3)    x = DarknetBlock(x, 64, 1)    x = DarknetBlock(x, 128, 2)    x = x_36 = DarknetBlock(x, 256, 8)    x = x_61 = DarknetBlock(x, 512, 8)    x = DarknetBlock(x, 1024, 4)    return tf.keras.Model(inputs, (x_36, x_61, x), name=name)  def YoloConv(filters, name=None):    def yolo_conv(x_in):        if isinstance(x_in, tuple):            inputs = Input(x_in[0].shape[1:]), Input(x_in[1].shape[1:])            x, x_skip = inputs            x = DarknetConv(x, filters, 1)            x = UpSampling2D(2)(x)            x = Concatenate()([x, x_skip])        else:            x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        return Model(inputs, x, name=name)(x_in)    return yolo_conv  def YoloOutput(filters, anchors, classes, name=None):    def yolo_output(x_in):        x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, anchors * (classes + 5), 1, batch_norm=False)        x = Lambda(lambda x: tf.reshape(x, (-1, tf.shape(x)[1], tf.shape(x)[2],                                        anchors, classes + 5)))(x)        return tf.keras.Model(inputs, x, name=name)(x_in)    return yolo_outputdef yolo_boxes(pred, anchors, classes):    grid_size = tf.shape(pred)[1]    box_xy, box_wh, score, class_probs = tf.split(pred, (2, 2, 1, classes), axis=-1)    box_xy = tf.sigmoid(box_xy)    score = tf.sigmoid(score)    class_probs = tf.sigmoid(class_probs)    pred_box = tf.concat((box_xy, box_wh), axis=-1)    grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))    grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)    box_xy = (box_xy + tf.cast(grid, tf.float32)) /  tf.cast(grid_size, tf.float32)    box_wh = tf.exp(box_wh) * anchors    box_x1y1 = box_xy - box_wh / 2    box_x2y2 = box_xy + box_wh / 2    bbox = tf.concat([box_x1y1, box_x2y2], axis=-1)        return bbox, score, class_probs, pred_box

Теперь определим функцию подавления не-максимумов.

def nonMaximumSuppression(outputs, anchors, masks, classes):    boxes, conf, out_type = [], [], []    for output in outputs:        boxes.append(tf.reshape(output[0], (tf.shape(output[0])[0], -1, tf.shape(output[0])[-1])))        conf.append(tf.reshape(output[1], (tf.shape(output[1])[0], -1, tf.shape(output[1])[-1])))        out_type.append(tf.reshape(output[2], (tf.shape(output[2])[0], -1, tf.shape(output[2])[-1])))    bbox = tf.concat(boxes, axis=1)    confidence = tf.concat(conf, axis=1)    class_probs = tf.concat(out_type, axis=1)    scores = confidence * class_probs      boxes, scores, classes, valid_detections = tf.image.combined_non_max_suppression(        boxes=tf.reshape(bbox, (tf.shape(bbox)[0], -1, 1, 4)),        scores=tf.reshape(            scores, (tf.shape(scores)[0], -1, tf.shape(scores)[-1])),        max_output_size_per_class=100,        max_total_size=100,        iou_threshold=yolo_iou_threshold,        score_threshold=yolo_score_threshold)      return boxes, scores, classes, valid_detections

Основная функция:

def YoloV3(size=None, channels=3, anchors=yolo_anchors,            masks=yolo_anchor_masks, classes=80, training=False):    x = inputs = Input([size, size, channels])    x_36, x_61, x = Darknet(name='yolo_darknet')(x)    x = YoloConv(512, name='yolo_conv_0')(x)    output_0 = YoloOutput(512, len(masks[0]), classes, name='yolo_output_0')(x)    x = YoloConv(256, name='yolo_conv_1')((x, x_61))    output_1 = YoloOutput(256, len(masks[1]), classes, name='yolo_output_1')(x)    x = YoloConv(128, name='yolo_conv_2')((x, x_36))    output_2 = YoloOutput(128, len(masks[2]), classes, name='yolo_output_2')(x)    if training:        return Model(inputs, (output_0, output_1, output_2), name='yolov3')    boxes_0 = Lambda(lambda x: yolo_boxes(x, anchors[masks[0]], classes),                  name='yolo_boxes_0')(output_0)    boxes_1 = Lambda(lambda x: yolo_boxes(x, anchors[masks[1]], classes),                  name='yolo_boxes_1')(output_1)    boxes_2 = Lambda(lambda x: yolo_boxes(x, anchors[masks[2]], classes),                  name='yolo_boxes_2')(output_2)    outputs = Lambda(lambda x: nonMaximumSuppression(x, anchors, masks, classes),                  name='nonMaximumSuppression')((boxes_0[:3], boxes_1[:3], boxes_2[:3]))    return Model(inputs, outputs, name='yolov3')

Функция потерь:

def YoloLoss(anchors, classes=80, ignore_thresh=0.5):    def yolo_loss(y_true, y_pred):        pred_box, pred_obj, pred_class, pred_xywh = yolo_boxes(            y_pred, anchors, classes)        pred_xy = pred_xywh[..., 0:2]        pred_wh = pred_xywh[..., 2:4]        true_box, true_obj, true_class_idx = tf.split(            y_true, (4, 1, 1), axis=-1)        true_xy = (true_box[..., 0:2] + true_box[..., 2:4]) / 2        true_wh = true_box[..., 2:4] - true_box[..., 0:2]        box_loss_scale = 2 - true_wh[..., 0] * true_wh[..., 1]        grid_size = tf.shape(y_true)[1]        grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))        grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)        true_xy = true_xy * tf.cast(grid_size, tf.float32) - \            tf.cast(grid, tf.float32)        true_wh = tf.math.log(true_wh / anchors)        true_wh = tf.where(tf.math.is_inf(true_wh),                      tf.zeros_like(true_wh), true_wh)        obj_mask = tf.squeeze(true_obj, -1)        true_box_flat = tf.boolean_mask(true_box, tf.cast(obj_mask, tf.bool))        best_iou = tf.reduce_max(intersectionOverUnion(            pred_box, true_box_flat), axis=-1)        ignore_mask = tf.cast(best_iou < ignore_thresh, tf.float32)        xy_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_xy - pred_xy), axis=-1)        wh_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_wh - pred_wh), axis=-1)        obj_loss = binary_crossentropy(true_obj, pred_obj)        obj_loss = obj_mask * obj_loss + \            (1 - obj_mask) * ignore_mask * obj_loss        class_loss = obj_mask * sparse_categorical_crossentropy(            true_class_idx, pred_class)        xy_loss = tf.reduce_sum(xy_loss, axis=(1, 2, 3))        wh_loss = tf.reduce_sum(wh_loss, axis=(1, 2, 3))        obj_loss = tf.reduce_sum(obj_loss, axis=(1, 2, 3))        class_loss = tf.reduce_sum(class_loss, axis=(1, 2, 3))        return xy_loss + wh_loss + obj_loss + class_loss    return yolo_loss

Функция "преобразовать цели" возвращает кортеж из форм:

(    [N, 13, 13, 3, 6],    [N, 26, 26, 3, 6],    [N, 52, 52, 3, 6])

Где N число меток в пакете, а число 6 означает [x, y, w, h, obj, class] bounding box'а.

@tf.functiondef transform_targets_for_output(y_true, grid_size, anchor_idxs, classes):    N = tf.shape(y_true)[0]    y_true_out = tf.zeros(      (N, grid_size, grid_size, tf.shape(anchor_idxs)[0], 6))    anchor_idxs = tf.cast(anchor_idxs, tf.int32)    indexes = tf.TensorArray(tf.int32, 1, dynamic_size=True)    updates = tf.TensorArray(tf.float32, 1, dynamic_size=True)    idx = 0    for i in tf.range(N):        for j in tf.range(tf.shape(y_true)[1]):            if tf.equal(y_true[i][j][2], 0):                continue            anchor_eq = tf.equal(                anchor_idxs, tf.cast(y_true[i][j][5], tf.int32))            if tf.reduce_any(anchor_eq):                box = y_true[i][j][0:4]                box_xy = (y_true[i][j][0:2] + y_true[i][j][2:4]) / 2                anchor_idx = tf.cast(tf.where(anchor_eq), tf.int32)                grid_xy = tf.cast(box_xy // (1/grid_size), tf.int32)                indexes = indexes.write(                    idx, [i, grid_xy[1], grid_xy[0], anchor_idx[0][0]])                updates = updates.write(                    idx, [box[0], box[1], box[2], box[3], 1, y_true[i][j][4]])                idx += 1    return tf.tensor_scatter_nd_update(        y_true_out, indexes.stack(), updates.stack())def transform_targets(y_train, anchors, anchor_masks, classes):    outputs = []    grid_size = 13    anchors = tf.cast(anchors, tf.float32)    anchor_area = anchors[..., 0] * anchors[..., 1]    box_wh = y_train[..., 2:4] - y_train[..., 0:2]    box_wh = tf.tile(tf.expand_dims(box_wh, -2),                    (1, 1, tf.shape(anchors)[0], 1))    box_area = box_wh[..., 0] * box_wh[..., 1]    intersection = tf.minimum(box_wh[..., 0], anchors[..., 0]) * \    tf.minimum(box_wh[..., 1], anchors[..., 1])    iou = intersection / (box_area + anchor_area - intersection)    anchor_idx = tf.cast(tf.argmax(iou, axis=-1), tf.float32)    anchor_idx = tf.expand_dims(anchor_idx, axis=-1)    y_train = tf.concat([y_train, anchor_idx], axis=-1)    for anchor_idxs in anchor_masks:        outputs.append(transform_targets_for_output(            y_train, grid_size, anchor_idxs, classes))        grid_size *= 2    return tuple(outputs) # [x, y, w, h, obj, class]def preprocess_image(x_train, size):    return (tf.image.resize(x_train, (size, size))) / 255

Теперь мы можем создать нашу модель, загрузить веса и названия классов. В COCO датасете их 80.

yolo = YoloV3(classes=num_classes)load_darknet_weights(yolo, weightyolov3)yolo.save_weights(checkpoints)class_names =  ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck",    "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",    "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe",    "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",    "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",    "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl",    "banana","apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut",    "cake","chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop",     "mouse","remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",    "refrigerator","book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]def detect_objects(img_path, white_list=None):    image = img_path     # Путь к изображению.    img = tf.image.decode_image(open(image, 'rb').read(), channels=3)    img = tf.expand_dims(img, 0)    img = preprocess_image(img, size)    boxes, scores, classes, nums = yolo(img)    img = cv2.imread(image)    img = draw_outputs(img, (boxes, scores, classes, nums), class_names, white_list)    cv2.imwrite('detected_{:}'.format(img_path), img)    detected = Image.open('detected_{:}'.format(img_path))    detected.show()    detect_objects('test.jpg', ['bear'])

Итог

В этой статье мы поговорили об отличительных особенностях YOLOv3 и её преимуществах перед другими моделями. Мы рассмотрели способ реализации с использованием TensorFlow 2.0 (TF должен быть не менее версией 2.0).

Ссылки

Подробнее..

Quantization Aware Training. Или как правильно использовать fp16 inference в TensorRT

21.05.2021 12:08:14 | Автор: admin

Low-precision inference в TensorRT сегодня - мастхэв, бест практис и прочие иностранные. Сконвертить из TensorFlow легко, запустить легко, использовать fp16 легко. Да и КПД выше, чем у pruning или distillation. На первый взгляд всё работает идеально. Но на самом деле всё ли так гладко? Рассказываем, как мы в TrafficData споткнулись об fp16, встали и написали статью.

Если ты читаешь эту статью ради подробного туториала о запуске TensorRT, то его тут нет. Он есть тут. Здесь же про опыт применения и несколько важных моментов, о которых не говорят в официальной документации.

Что за зверь ваш low-precision?

float16

И так, low-precision inference - запуск нейронных сетей в типе пониженной точности. Но зачем это нужно? По умолчанию все фреймворки учат и сохраняют модели в типе float32. Оказывается, что количество знаков во fp32 - часто избыточно. Ну а зачем нам эти сотни знаков после запятой? Можно просто скастовать fp32 веса во fp16, чтобы получить ускорение прямого прогона и уменьшение используемой памяти в 2 раза. При этом сохранив исходную точность модели. Единственное условие - наличие тензорных ядер в вашем GPU.

int8 и прочее

Кроме fp16 с простым кастованием есть много идей по более оптимальному использованию бит в 16-битном значении. Просто чтобы напомнить:

Но этого мало. Использование нейронных сетей в высоконагруженных системах и мобильных платформах заставляет еще сильнее ужимать сети и ускорять инференс. Добро пожаловать в мир int8 и int4. Да, в них квантуют. Да, в int8 всего 256 значений. Да, это работает. Со своими сложностями, конечно - здесь уже просто так не кастанёшь, как в случае с fp16. Нужно внимательно изучать распределения значений в слоях, чтобы эффективно использовать предоставленный небольшой диапазон значений.

Объясню, почему мы не смотрим на 8/4 битные квантизации. Дело в том, что здесь не обойтись без потери точности. Например, тут говорят как оптимально юзать int4 и радуются, что потеряли не 15%, а 8% точности. Или вот красноречивая табличка от Nvidia о западении точности при использовании int8:

Я слышал на конференциях, что на такую потерю точности нормально смотрят в мобильных решениях. Особенно, если это какой-то развлекательный контент типа переноса стилей на GANax и тд. Ну изменился стиль немного - ничего страшного.
В нашем решении каждый процент точности - наше золотце. И пока мы не научились не терять их в int8.

TensorRT

Если у вас мобильные решения или просто инференс на CPU, то попробуйте TensorFlow Lite. Но в основном, говоря про low-precision inference в проде, сегодня имеют ввиду TensorRT - кроссплатформенный SDK для супер-быстрой работы на GPU от Nvidia. TensorRT легко превращает ваши модели в оптимизированные Engines. Сконвертить можно из любого нейросетевого фреймворка через ONNX. Engine - очень важная сущность в TensorRT. При билде происходит оптимизация под текущий GPU - на других GPU engine либо не запустится, либо будет работать неоптимально. Короче говоря, есть ряд параметров, которые нужно знать или задать заранее:

  • GPU. На чём собрали Engine, на том пусть он и работает. Но допустим общий билд для карточек одного семейства - Turing или Ampere. Например, мы билдили Engine для RTX 2060 и он замечательно работает на RTX 2080 Super. Создание отдельного Engine для RTX 2080 Super существенного ускорения не создает.

  • BatchSize. Нужно задать максимальный - для него и будет соптимизирован Engine. В рантайме можно подавать батчи размером меньше максимального, но это будет неоптимально.

  • InputSize. Мы работаем с изображениями. И размер входного изображения иногда может меняться во время рантайма. Но TRT требует его задавать жестко, что логично. Да, есть возможность задать минимальный и максимальный размеры, а TRT создаст несколько профилей оптимизации. Но всё же это не так гибко, как в TensorFlow, а иногда нужно.

  • Precision. Собственно мы можем задать fp32/fp16/int8. Первые два различаются лишь выбором флага. С int8 я мало экспериментировал. Но судя по документации, отличие лишь в необходимости калибровочного датасета - набора картинок, на основании которого TRT определит распределения значений на разных слоях.

Ну и под конец еще добавлю, что в рантайме эти движки отжирают лишь необходимый минимум GPU RAM и замечательно работают параллельно (если правильно работать с TensorRT Context в вашем коде рантайма).

Контекст задачи

Окей, чтобы было понятнее, какие проблемы возникли, пара слов о нашем продукте. Мы пилим TrafficData - ПО для оценки трафика c камер и дронов, в рилтайме и постфактум, в дождь и снег. Нам важно, чтобы ночью детектилось вот так:

И не хуже.

На opentalks.ai2020 мы рассказывали, как, используя Pruning и физичность данных, ускорили обработку в 4 раза и не потеряли в точности. Статью про Pruning я уже выкладывал. Но сегодня давайте поговорим конкретно про low-precision inference.

Как мы запустились и потеряли нежные фичи

Скачивая либы TensorRT, бонусом вы получаете набор примеров с кодом для самых разных архитектур и ситуаций. Для билда движков мы использовали пример SampleUffSSD (UFF - универсальный формат описания сети, через который мы конвертили наши .pb), cлегка его закастомив под входной тензор от YOLO. И хотя TensorRT очень много обновляется и всё больше новых интересных слоев поддерживает, тогда мы запускались на версии, где не было реализации ResizeBilinear операции для Upsample слоя. И мы накостылили Conv2DTranspose вместо него, чтобы не писать кастомный слой. Первая сконверченная модель была радостью, как и её скорость работы.

Даже если перейти с fp32 из TF в fp32 TRT, то уже получается неслабое ускорение - на 15-20%. В конце концов TRT использует и много других оптимизаций, например горизонтальные, вертикальные и любые другие LayerFusion.

Для инференса мы закастомили пример trtExec, обернув его для использования в .NET коде. На вход - байты изображения, на выходе - нераспарсенные байты выхода YOLO. Здесь аккуратно работайте с CudaStream и ExecutionContext. Тогда ни память не утечет, ни потоки обработки не закорраптятся.

И так, мы реализовали TensorRT fp16 inference. Сбилдили движки для разных карточек. Прогнали основные тесты - колебания точности в пределах погрешности. И всё замечательно работало несколько месяцев. А дальше - история.
10:00. Звонок клиента:
- У нас тут на одном ролике TrafficData плохо работает - машинки двоятся.
- Окей, скиньте ролик разберемся.
Смотрим ролик - да, проблема есть. Ролик с тенями и на нём тени отмечаются, как второе авто.

13:00. Добрали изображения в основной датасет. Поставили доучиться с низким LR.

16:00. Тестим на версии с инференсом в TensorFlow - всё замечательно. Билдим новый Engine. Тестим на версии с инференсом в TensorRT - опять машины двоятся:

17:00. Идём домой.

Следующее утро началось с мема:

Стало очевидно, что проблема в TensorRT, а конкретно - в преобразовании весов во fp16. Мы проверили еще несколько других роликов со сложными условиями и увидели, что после преобразования во fp16 проблемы появились и в других местах. Стали появляться пропуски детекции на ночных видео, некоторые билборды стали определяться как авто. Короче вот так мы потеряли нежные, но важные фичи, про которые оригинальная сеть во fp32 знала, а вот во fp16 успешно забыла. Что делать?

Quntization Aware Training. Учи на том, на чем будет работать

Подсознательно мы сразу понимали, что если мы обучаем на fp32, а потом инференсим на fp16, то выйдет неприятная вещь. Вот эти жалкие циферки далеко после запятой потеряны и так влияют. Тогда зачем мы их учили на каждом батче? Идея Quntization Aware Training крайне проста - учи и помни о том типе, в котором будешь инференсить. Т.е. в типе fp16 должны быть все веса сверток, активаций и градиентов. Не удивляйтесь, если первые запуски в TensorFlow окажутся с NaN-лоссом. Просто внимательно инспектируйте происходящее. Мы потратили пару недель, переписали всё обучение на fp16 и проблема была решена.

Как в Tensorflow 2.0?

Тут небольшое отступление о том, как мы были рады обновлению TF2.0. Работая под TF1.15 мы кусали локти, заставляя запуститься обучение во fp16, переписывая слои. Но это заработало. А потом пришел TF2.0 - используешь tf.train.experimental.enable_mixed_precision_graph_rewrite над оптимизатором и всё заводится, как моя Lada Granta. Но всё же стоит обратить внимание на whitelist - не все ноды по умолчанию будут работать во fp16. Часть стоит поправить руками. Ну и дополнительный бонус - огромная экономия памяти, которой не получалось в TF1.15. Батч-сайз для нашей кастомной YOLOv4.5 увеличился в 2 раза - с 4 до 8. Больше батч - лучше градиенты.

Выводы

Fp16 inference - это здорово. Только не стоит забывать про Quntization Aware Training, если вы хотите сохранить точность оригинальной модели. Это позволило нам сделать еще шаг в сторону оптимизации наших и клиентских мощностей:

Что особенно важно в годы дефицита чипов и дорогих GPU. Я всё же за использование GPU в тех местах, где они приносят пользу людям, автоматизируя что-то. А не там, где они приносят прибыль, делая деньги из подогретого воздуха.

А вообще вся тематика ускорения инференса сетей сегодня - очень интересное поле для экспериментов. Хочется попробовать десятки новых способов Pruning, Distillation или квантования в int4, но всех Баксов Банни не догонишь. Пробуйте новое, но не забывайте отдыхать.

Подробнее..

Рецепт обучения нейросетей

06.02.2021 02:09:12 | Автор: admin

Перевод статьи A Recipe for Training Neural Networks от имени автора (Andrej Karpathy). С некоторыми дополнительными ссылками.

Также доступна версия на украинском языке в личном блоге: Рецепт навчання нейрнонних мереж.

Несколько недель назад я опубликовал твит на тему частые ошибки с нейросетями, перечислив несколько общих ошибок принадлежащих к обучению нейронных сетей. Твит получил несколько больше взаимодействий чем я ожидал (включая целый вебинар). Действительно, многие заметили большой разрыв между тем вот как работает слой свертки и наша сверточная сеть достигает результатов произведения искусства.

Поэтому я подумал, что будет весело смести пыль со своего блога, чтобы раскрыть свой твит в более объемном формате, которого и заслуживает эта тема. Однако, вместо того чтобы углубиться в перечень еще большего количества частых ошибок или их конкретизацию, я хотел бы копнуть глубже и поговорить о том, как обойти эти ошибки целиком (или исправить их очень быстро). Фокус в том, чтобы следовать определенному процессу, который, насколько я могу сказать, нечасто документируется. Давайте начнем с двух важных наблюдений, которые побудили к этому.

1) Нейронные сети это дырявая абстракция

Это вроде бы просто начать учить нейронные сети. Несколько библиотек и фреймворков гордятся показом магических 30-строчных кусков кода которые решают проблемы с вашими данными, давая (ложные) впечатление, что это все работает из коробки. Привычно видеть подобные вещи:

>>> your_data = # подставьте свой датасет здесь>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)# покорите мир здесь

Эти библиотеки и примеры активируют часть нашего мозга которая привычна к стандартным программам - место где чистые API и абстракции часто достижимы. Например, библиотека requests:

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))>>> r.status_code200

Круто! Смелый разработчик взял на себя бремя понимание строк запросов, URL, GET / POST запросов, HTTP соединений и т.д., и во многом скрыл сложность за несколькими строками кода. Это то, с чем мы знакомы и ожидаем. К сожалению, нейронные сети не похожи на это. Они не "готовая" технология, когда вы немного отклонились от обучения классификатора ImageNet. Я пытался указать на это в своей публикации "Да вы должны понимать метод обратного распространения ошибки" ("Yes you should understand backprop"), выбрав метод обратного распространения ошибки и назвав его "дырявой абстракцией", но ситуация, к сожалению, гораздо сложнее. "Обратное распространение ошибки" + "Стохастический градиентный спуск не делает вашу нейронную сеть магически работающей. Пакетная нормализация не заставляет ее магически сходиться быстрее. Рекуррентные нейронные сети не позволяют магически "вставить" текст. И только потому, что вы можете сформулировать вашу проблему в форме "обучение с подкреплением" не означает, что вы должны это делать. Если вы настаиваете на использовании технологии, не зная как она работает, вы, вероятно, потерпите неудачу. Что подводит меня к

2) Обучение нейронных сетей ломается молча

Когда вы неправильно написали или настроили код вы часто получаете определенное исключение. Вы передали целое число там где ожидается строка. Функция ожидает только 3 аргумента. Этот импорт неудачный. Тот ключ не существует. Количество элементов в двух списках не ровен. В довесок, часто возможно написать юнит-тесты для определенного функционала.

Это только начало, когда дело касается тренировки нейронных сетей. Все может быть синтаксически верно, но не упорядочено вместе должным образом, и об этом действительно трудно сказать (компилятору или интерпретатору). "Возможная поверхность ошибок" большая, логическая (в отличие от синтаксической) и очень сложная для юнит-тестирования. Например, вы забыли перевернуть слой обозначений когда переворачивали изображения при аугментации данных. Ваша сеть все еще (что шокирует) может работать достаточно хорошо, потому что ваша сеть может внутренне научиться выявлять перевернутые изображения, а затем переворачивать свои прогнозы. Или, возможно, ваша авторегресивная модель случайно принимает то, что она пытается предсказать, как информацию на входе через незаметную ошибку. Или вы пытались обрезать свои градиенты, но вместо этого обрезали потерю, что повлекло игнорирование выбросов, во время обучения. Или вы инициализируете ваши весы с предварительного обучения, но не используете исходное среднее. Или вы просто испортили настройки регуляризации, скорости обучения, размера модели, и т.д. Поэтому ваша неправильно настроена нейронная сеть выбросит исключение, только если вам повезет; В основном она обучается, но молча работает чуть хуже.

Как результат, (и это ооочень сложно переоценить) "быстрый и яростный" подход к обучению нейронных сетей не работает и приводит лишь к страданиям. Сейчас страдания являются вполне естественной частью того, чтобы в результате нейронная сеть работала хорошо, но их можно смягчить, если быть вдумчивыми, защищенными, параноидальными и одержимыми визуализацией практически всего. Качество, которое на моем опыте больше всего коррелирует с успехом в глубоком обучении, - это терпение и внимание к деталям.

Рецепт

На фоне вышеупомянутых двух фактов, я разработал для себя конкретный процесс, которого я придерживаюсь, применяя нейронную сеть к новой проблеме, и который я попробую описать. Вы увидите, что эти два принципа воспринимаются очень серьезно. В частности, проходит построение от простого к сложному и на каждом шагу мы делаем определенные гипотезы о том, что произойдет, а потом либо проверяем их экспериментом, или исследуем, пока не найдем какую-нибудь проблему. То, что мы пытаемся всеми силами предотвратить - это введение большого количества "непроверенной" сложности сразу, что обязательно приведет к ошибкам или неправильной конфигурации, поиски которых будут длиться вечно. Если бы процесс написания кода нейронной сети был бы подобным обучению нейросети (здесь написания кода нейросети используется как прямая аналогия к обучению нейросети), то вы хотели бы использовать очень малую скорость обучения и угадывать, а затем оценивать полный набор тестов после каждой итерации.

1. Cтаньте едиными c данными

Первый шаг к обучению нейронных сетей - это вообще не касаться кода нейронной сети, а взамен начать с тщательной проверки ваших данных. Этот шаг критический. Я люблю тратить много времени (измеряется в часах), проверяя тысячи примеров, понимая их распределение и ища закономерности. К счастью, ваш мозг хорошо с этим справляется. Однажды я обнаружил, что данные содержат примеры которые повторяются. В другой раз я обнаружил поврежденные изображения / разметку. Я ищу дисбаланс данных и смещения. Обычно я также обращаю внимание на свой собственный процесс классификации данных, который намекает на виды архитектур которые мы со временем изучим. В качестве примера - достаточно локальных особенностей, или нам нужен глобальный контекст? Сколько существует вариаций и какую форму они принимают? Какая вариация ошибочная и может быть предварительно обработана? Имеет ли значение пространственное расположение или мы хотим его усреднить (с помощью операции average pool)? Насколько важны детали и насколько сильно мы можем позволить себе уменьшить размер изображений? Насколько зашумленная разметка?

Кроме этого, поскольку нейронная сеть является фактически сжатой / скомпилированной версией вашего набора данных, вы сможете просмотреть свои (ложные) прогнозы в вашей сети и понять, откуда они могут поступать. И если ваша сеть дает вам прогноз, который не соответствует тому, что вы видели в данных, то что-то пошло не так.

Получив понимание качественной характеристики, также хорошей идеей является написание какого-то простого кода для осуществления поиска / фильтрации / сортировки за любой возможной характеристикой (например, по типу метки, размеру аннотаций, количеству аннотаций и т.д.) и визуализировать их распределение и выбросы по любой оси. Выбросы почти всегда разоблачают какие-нибудь баги в данных или в их подготовке.

2. Настройте сквозной скелет обучения / оценки + получите простой базис (базовую модель)

Теперь, когда мы поняли наши данные, можем ли мы добраться до нашей чрезвычайно крупномасштабной ASPP FPN ResNet и начать обучение великолепных моделей? Точно нет. Это путь к страданиям. Наш следующий шаг - создать полный скелет обучение + оценка и завоевать доверие к его правильности путем серии экспериментов. На этом этапе лучше выбрать какую-то простую модель, которую невозможно как-то испортить - например линейный классификатор или очень крошечную сверточную сеть. Мы хотим обучать сеть, визуализировать потери, любые другие показатели (например, точность), моделировать прогнозы и проводить ряд экспериментов по отключению частей сети (при этом выдвигать гипотезы как это повлияет на результаты) на всем пути.

Советы и подсказки на этом этапе:

  • зафиксируйте случайное начальное значение. Всегда используйте фиксированное случайное начальное значение, чтобы гарантировать аналогичный результат при повторном запуске. Это устраняет фактор вариативности и поддержит вашу уверенность.

  • упрощайте. Не забудьте отключить любую ненужную вычурность. В качестве примера, на этом этапе выключите любую аугментацию данных. Аугментация данных - это стратегия регуляризации, которую мы можем включить позже, но пока это просто еще одна возможность внести какую-то глупую ошибку.

  • добавьте значащие цифры в вашей оценки. При построении графика тестовых потерь проведите оценку по всему (большому) набору тестов. Не просто складывайте тестовые потери по партиям, а затем полагайтесь на их сглаживания в Tensorboard. Мы преследуем правильность и серьезно настроены уделить время для сохранения уверенности.

  • проверяйте потери в начале. Убедитесь, что показатель потери начинается с правильного значения. Например, если вы правильно инициализирует свой конечный слой, то у вас должно получиться -log(1 / n_classes) для функции softmax при инициализации. Те же значения по умолчанию можно получить для регрессии L2, потерь Губера и тому подобное.

  • инициализируйте верно. Правильно инициализируйте веса конечного слоя. Например, если вы регрессируете некоторые значения, которые имеют среднее значение 50, тогда инициализируйте окончательное смещение к 50. Если у вас несбалансированный набор данных с соотношением 1:10, установите смещение на своих логитах так, чтобы ваша сеть давала предсказания 0.1 при инициализации. Правильная их установка ускорит сходимость и устранит кривые потерь в виде "хоккейной клюшки", где в первые несколько итераций ваша сеть в основном лишь изучает смещения.

  • человеческий базис. Отслеживайте и другие показатели, кроме потерь, которые можно интерпретировать и проверить человеком (например, точность). По возможности оценивайте собственную (человеческую) точность и сравнивайте с ней. Кроме того, дважды аннотируйте тестовые данные и для каждого примера рассмотрите одну аннотацию как предсказания, а вторую как основную правду.

  • независимый от входных значений базис. Обучайте независимый от входных значений базис (например, простой является установка всех входных значений на ноль). Это должно работать хуже, чем тогда, когда вы фактически подключаете свои данные, не обнуляя их. Действительно так? Действительно ваша модель вообще учится извлекать любую информацию из входных данных?

  • переучивайте на одной партии. Делайте переобучение на одной партии лишь несколькими примерами (например, только двумя). Для этого мы увеличиваем объем нашей модели (например, добавляем слои или фильтры) и проверяем, что мы можем достичь самых низких достижимых потерь (например, нулевых). Мне также нравится визуализировать на одном и том же графике как размеченную информацию, так и прогноз, чтобы убедиться, что они идеально выравниваются, как только мы достигнем минимальных потерь. Если этого не произошло, где-то есть ошибка, и мы не можем перейти к следующему этапу.

  • проверяйте уменьшения потерь на тренировочной выборке. Думаю, ваш набор данных будет не очень объемным, так как вы работаете с игрушечной моделью. Попробуйте немного увеличить его объем. Значение потерь на тренировочной выборке снизились как следует?

  • визуализируйте непосредственно перед входом нейросети. Однозначно правильное место для визуализации ваших данных находится непосредственно перед вашим y_hat = model (x) (или sess.run в Tensorflow). То есть - вы должны визуализировать именно то, что попадает в вашу сеть, декодируя этот необработанный тензор данных и меток в виде какой-то визуализации. Это единственный "источник истины". Я не могу сосчитать, сколько раз это меня спасало и проявляло проблемы с предварительной обработкой и аугментацией данных.

  • визуализируйте динамику прогнозов. Мне нравится визуализировать прогнозы моделей на фиксированной тестовой партии во время обучения. "Динамика" движения этих прогнозов даст вам невероятно хорошую интуицию о том, как прогрессирует обучение. В основном можно почувствовать, как сеть борется за размещение ваших данных, если она как-то колеблется, показывая нестабильность. Очень низкая или очень высокая скорость обучения также легко различимы по величине дрожи.

  • используйте метод обратного распространения ошибки для отслеживания зависимостей. Ваш код для глубокого обучения часто может содержать сложные, векторизованные и трансляционные операции. Достаточно распространенная ошибка, с которой я сталкивался несколько раз, заключается в том, что люди достигают этого неправильно (например, они используют view, а не transpose / permute) и нечаянно смешивают информацию в измерении размера пакета. Удручает тот факт, что ваша сеть, как правило, все равно способна хорошо учиться, потому что она научится игнорировать данные из других примеров. Одним из способов налаживания этой (и других связанных с этим проблем) является установление функции потери как чего-то тривиального, такого как сумма всех выходов примера i, запуск обратного прохода до входного сигнала и обеспечения получения ненулевого градиента только на i-м входе. Ту же стратегию можно использовать, чтобы убедиться, что ваша авторегресивная модель в момент времени t зависит только от 1..t-1. В общем, градиенты дают вам информацию о том, что и от чего зависит в вашей сети, это может быть полезно для отладки.

  • обобщайте частный случай. Это больше похоже на совет обобщать код, но я часто видел, как люди делают ошибки, когда откусывают больше, чем могут жевать, стараясь писать относительно общую функциональность с нуля. Мне нравится писать очень конкретную функцию для того, что я делаю сейчас, заставить это работать, а потом обобщить ее позже, убедившись, что я получу тот же результат. Часто это касается векторизации кода, где я почти всегда выписываю полностью циклическую версию, а уже потом превращаю ее в векторизованный код по одному циклу.

3. Переобучайте

На этом этапе мы должны хорошо понимать набор данных, и мы имеем полный конвейер обучение + оценки. Для любой данной модели мы можем (воспроизводимо) вычислить метрику, которой мы доверяем. Мы также вооруженны результатами нашего независимого от входных данных базиса, результатами нескольких простых базисов (нам лучше победить именно их), и мы имеем приблизительное ощущение производительности человека (мы надеемся достичь этого уровня). Текущий этап направлен на итерации в направлении хорошей модели.

Подход, который я люблю применять к поиску хорошей модели, состоит из двух этапов: сначала получить модель, достаточно большую, чтобы она могла переучиться (то есть сосредоточить внимание на значении потерь тренировочной выборки), а затем регуляризировать ее должным образом (ухудшить некоторые значения потерь учебной выборки, чтобы улучшить значение потерь при проверке). Причиной, почему мне нравятся эти два этапа, является то, что если мы не можем достичь низкого уровня ошибок с любой моделью вообще, это может вновь указывать на какие-то проблемы, ошибки или неправильную конфигурацию.

Несколько советов и подсказок на этом этапе:

  • подбор модели. Чтобы достичь хороших значений потерь обучающей выборки, вы должны выбрать соответствующую архитектуру данных. Когда дело доходит до ее выбора, мой первый совет: Не будьте героем. Я видел много людей, которые стремятся сойти с ума в креативности подбора лего-блоков из набора инструментов нейронных сетей в процессе создания различных экзотических архитектур, которые имеют смысл только для них. На первых этапах проекта всеми силами сопротивляйтесь этому искушению. Я всегда советую людям просто найти наиболее похожую научную работу и скопировать ее простейшую архитектуру, которая обеспечивает хорошие показатели. Например, если вы классифицируете изображения, не будьте героем, а просто скопируйте ResNet-50 для первого запуска. Вы сможете делать что-то более специфическое позже и победить этот пункт.

  • Adam (метод адаптивной оценки моментов) безопасен. На ранних стадиях установления базиса мне нравится использовать Adam со скоростью обучения 3e-4. По моему опыту, Adam гораздо лояльнее к гиперпараметрам, включая плохую скорость обучения. Для сверточных нейросетей хорошо настроенный метод стохастического градиента (SGD) почти всегда немного превосходит Adam, но область оптимальной скорости обучения гораздо более узкая и зависит от задачи. (Примечание. Если вы используете рекуррентные нейросети и связанные с ними модели обработки последовательностей, то чаще используют Adam. Опять же, на начальном этапе своего проекта не будьте героем и соблюдайте самые популярные статьи.)

  • усложняйте только по одному. Если у вас есть несколько сигналов для подключения к вашему классификатору, я бы посоветовал вам присоединить их один за другим и каждый раз убеждаться, что вы получаете повышение производительности, которое вы ожидали. Не бросайте ведро помоев на свою модель в самом начале. Есть и другие способы наращивания сложности - например, вы можете попробовать подключить сначала изображения поменьше, а позже увеличить их и т.д.

  • не доверяйте коэффициенту уменьшения скорости обучения по умолчанию. Если вы переделываете код с какой-то другой задачи, всегда будьте очень осторожны со снижением скорости обучения. Вы не только хотели бы использовать различные графики снижения скорости обучения для различных проблем, но - что еще хуже - в типовой реализации снижение будет базироваться на текущем номере эпохи, который может широко варьироваться просто в зависимости от размера вашего набора данных. Например, ImageNet замедлится в 10 раз на 30-й эпохе. Если вы не обучаетесь с ImageNet (имеется в виду размер датасета), вы, почти наверняка, этого не хотите. Если вы не будете осторожны, ваш код может тайком сводить вашу скорость обучения к нулю слишком рано, не позволяя вашей модели сходиться. В своей работе я всегда полностью выключаю уровень снижения скорости обучения (использую постоянную скорость обучения) и настраиваю его в самом конце.

4. Регуляризируйте

В идеале, мы сейчас находимся в том месте, где есть большая модель, которая подходит как минимум для учебного набора. Сейчас настало время его регуляризировать и получить определенную точность проверки, отказавшись от части точности на обучающей выборке. Некоторые советы и подсказки:

  • получите больше данных. Во-первых, безусловно лучшим способом регуляризирования модели в любом практической среде является добавление большего количества реальных учебных данных. Очень распространенной ошибкой является проведение многих инженерных циклов, пытаясь выжать сок из небольшого набора данных, когда вместо этого можно было собирать больше данных. Насколько мне известно, добавление дополнительных данных является едва ли не единственным гарантированным способом монотонно улучшать производительность хорошо настроенной нейронной сети почти неограниченно долго. Остальные - это ансамбли нейросетей (если вы можете себе позволить), но это ограничивается ~ 5-ю моделями.

  • аугментация данных. Следующим лучшим способом после реальных данных является полу фальшивые данные - попробуйте более агрессивную аугментацию данных.

  • креативная аугментация. Если полу фальшивые данные не помогли, фейковые данные также могут что-то сделать. Люди находят творческие способы расширения наборов данных; Например, рандомизация доменов, использование моделирования, умные гибриды, такие как вставка (потенциально смоделированная) данных у сцены или даже GAN.

  • предварительно обучайте. Редко когда-нибудь вредит использовать предварительно обученную сеть, если вам позволяет ситуация, даже если у вас достаточно данных.

  • придерживайтесь контролируемого обучения (обучение с учителем). Не переоценивайте предварительное обучение без присмотра (без учителя). В отличие от того, что рассказывается в той заметке в блоге от 2008 года [не могу понять о каком сообщении тут идет речь], насколько мне известно, нет версий, которые показывают хорошие результаты на современных задачах компьютерного зрения (хотя NLP, кажется, вполне хорошо справляется вместе с BERT и компанией сегодня, вполне вероятно благодаря умышленному характеру текста и высшему соотношению сигнал / шум).

  • уменьшайте входную размерность. Удалите примеры, которые могут содержать ложный сигнал. Любой добавленный ложный ввод - это лишь очередная возможность переобучить (когда ваша нейросеть заучит пример), если ваш набор данных невелик. Подобным образом, если детали низкого уровня не имеют большого значения, попробуйте передавать изображение меньшего размера.

  • уменьшайте размер модели. Во многих случаях вы можете использовать ограничения информативности участка в сети, чтобы уменьшить ее размер. В качестве примера, раньше было модно использовать слои с полным соединением поверх основы из ImageNet, но с тех пор они были заменены простым средним объединением (average pooling), устраняя тонну параметров в процессе.

  • уменьшайте размер партии. Через нормализацию внутри нормы партии меньшие размеры партии несколько соответствуют сильной регуляризации. Это связано с тем, что эмпирическое среднее / стандартное распределение для партии является более приблизительной версией полного среднего / стандартное распределение, поэтому изменение масштаба и смещения "раскачивают" вашу партию больше.

  • отсеивайте. Добавьте отсеивания. Используйте dropout2d (пространственное отсеивания) для сверточных сетей. Используйте это умеренно / осторожно, поскольку, кажется, отсеивания нехорошо работает при нормализации партии.

  • уменьшение веса. Увеличьте коэффициент уменьшения веса (эффект забывания).

  • ранняя остановка. Останавливайте обучение на основе измеренных валидационных потерь, чтобы поймать свою модель именно тогда, когда она собирается переобучиться (заучить примеры, а не изучить общие особенности).

  • попробуйте модель побольше. Я вспоминаю это последним и только после ранней остановки, ведь раньше я несколько раз обнаруживал, что большие модели со временем, конечно, переобучаются гораздо сильнее, но их "остановленная" эффективность часто может быть намного лучше, чем у моделей меньшего размера.

Наконец, чтобы получить дополнительную уверенность в том, что ваша сеть является разумным классификатором, я люблю визуализировать веса первого уровня сети и гарантировать, что вы получаете хорошие края, которые имеют смысл. Если ваши фильтры первого слоя похожи на шум, тогда что-то может быть не так. Подобным образом активации внутри сети иногда могут показывать странные артефакты и намекать на проблемы.

5. Тюнингуйте

Теперь вы должны быть "связаны" с вашим набором данных, изучая широкий простор моделей для архитектур, которые достигают низких потерь в ходе проверки. Несколько советов и подсказок для этого шага:

  • случайный поиск по сетке. Для одновременной настройки нескольких гиперпараметров может показаться соблазнительным использовать поиск по сетке, чтобы обеспечить охват всех настроек, но имейте в виду, что лучше вместо этого использовать случайный поиск. Интуитивно это связано с тем, что нейронные сети часто гораздо более чувствительны к одним параметрам, чем к другим. В общем, если параметр a важен, но изменение b не имеет эффекта, вы подбираете значение a более продуманно, чем в нескольких фиксированных точках несколько раз.

  • оптимизация гиперпараметров. Вокруг есть большое количество причудливых наборов инструментов для оптимизации байесовских гиперпараметров, и несколько моих друзей также сообщили об успехе с ними, но мой личный опыт состоит в том, что современный подход к изучению прекрасного и широкого пространства моделей и гиперпараметров заключается в использовании интерна :). Шучу.

6. Выжмите все соки

Найдя лучшие типы архитектур и гиперпараметров, вы все еще можете воспользоваться несколькими хитростями, чтобы выжать последние капли сока из системы:

  • ансамбли. Ансамбли моделей - это почти гарантированный способ получить 2% точности на чем-либо. Если вы не можете позволить себе вычисления во время тестирования, посмотрите на перегонку своего ансамбля в сеть, используя темные знания.

  • оставьте ее тренироваться. Я часто видел людей, которые соблазняются прекратить обучение моделей, когда потеря валидации, кажется, выравнивается. По моему опыту, сети продолжают тренироваться не интуитивно долго. Однажды я случайно покинул тренировку модели во время зимних каникул, и когда вернулся в январе, я увидел результат SOTA (state of the art - "современный уровень").

Вывод

Как только вы дойдете сюда, у вас будут все составляющие успеха: Вы глубоко понимаете технологию, набор данных и проблему, вы создали всю инфраструктуру обучения / оценки и достигли высокой уверенности в ее точности, вы исследовали все более сложные модели, получая улучшения производительности способами, которые вы предугадывали на каждом шагу. Теперь вы готовы прочитать много работ, попробовать большое количество экспериментов и получить свои результаты SOTA. Удачи!

Подробнее..

Перевод Как скопировать стиль Уорхола с помощью нейросети VGG-19, трансферного обучения и TensorFlow

11.02.2021 16:05:53 | Автор: admin

То что мы сделаем ещё называется Нейронный перенос стиля это метод смешивания двух изображений и создания нового изображения из изображения-контента путём копирования стиля другого изображения, которое называется изображением стиля. Созданное изображение часто называют стилизованным изображением.

В этой статье мы скопируем стиль Энди Уорхола с Мэрилин Диптихна наши фотографии. Уорхол создал диптих Монро в 1962 году, сначала раскрасив холст разными цветами, а затем разместив теперь знаменитое изображение Мэрилин поверх холста. Хотя Уорхол не является основателем поп-арта, он одна из самых влиятельных фигур в этом жанре.



Рис. 1. Мэрилин Диптих Уорхола, а на кдпв показан наш результат нейронного переноса стиля в поп-арт, которого мы добились с помощью сети VGG-19

Что касается технического аспекта туториала, вместо использования готовой сети Magenta мы используем предварительно обученную модель компьютерного зрения VGG-19 и настроим её. Таким образом, эта статья представляет собой руководство по переносному обучению, а также по компьютерному зрению. Применяя возможности трансферного обучения, мы можем достичь лучших результатов, если сможем правильно настроить модель и иметь широкий спектр дополнительных возможностей настройки.

переносное обучение это подраздел машинного обучения и искусственного интеллекта, цель которого применить знания, полученные в результате выполнения одной задачи (исходной задачи), к другой, но похожей задаче (целевой задаче).

Кратко расскажу о модели, которую мы будем настраивать: VGG-19.

VGG-19


VGG это свёрточная нейронная сеть с глубиной 19 слоев. Она была построена и обучена К. Симоняном и А. Зиссерманом в Оксфордском университете в 2014 году. Вся информация об этом есть в статье Very Deep Convolutional Networks for Large-Scale Image Recognition, опубликованной в 2015 году. Сеть VGG-19 обучена с использованием более одного миллиона изображений из базы данных ImageNet. Она обучалась на цветных изображениях размером 224x224 пикселей. Естественно, вы можете импортировать модель ImageNet с уже обученными весами. Эта предварительно обученная сеть может классифицировать до тысячи объектов. В этом туториале мы избавимся от верхней части, используемой для классификации, и добавим наши собственные дополнительные слои, чтобы её можно было использовать для нейронного переноса стиля. Вот официальная визуализация сети из научной работы:

Рис. 3. Иллюстрация сети VGG-19

Как я уже упоминал, чей стиль мог бы быть более культовым и более подходящим, чем стиль Энди Уорхолла для переноса в поп-арт. Мы будем использовать его культовую работу Мэрилин Диптих в качестве основы стиля и портретное фото из Unsplash в качестве основного контента:

Рис. 4. Мэрилин Диптих и выбранное для эксперимента фото

Уакзываем пути к изображениям


Используя TensorFlow, я могу написать get_files [получить файлы] с внешних URL-адресов. С помощью приведённого ниже кода я загружу изображения в свой блокнот Colab, одно для стиля, а другое для контента:


Масштабирование изображений


Поскольку наши изображения имеют высокое разрешение, нам необходимо масштабировать их, чтобы обучение не занимало слишком много времени. Приведённый ниже код преобразует данные изображения в подходящий формат, масштабирует изображение (не стесняйтесь изменять параметр max_dim) и создаёт новый объект, который можно использовать для загрузки в модель:


Загрузка изображений


Теперь, когда мы определяем нашу функцию img_scaler, мы можем создать функцию обёртывания для загрузки изображения из контуров изображения, которые мы установили выше, масштабировать их, чтобы ускорить обучение (с помощью вызова img_scaler()) и создть 4-мерный тензор, чтобы он подходил для VGG-19:


Теперь можно просто создать тензоры content_image и style_image, используя функции, которые мы перечислили выше:


Отображение изображения


Используя matplotlib, мы можем легко отобразить контент и изображения стиля рядом:


И вот вывод:

Рис. 5. Визуализация изображений контента и стиля

Теперь, когда у нас есть изображения, подготовленные для нейронного переноса стиля, мы можем создать нашу модель VGG-19 и подготовить её для точной настройки. Этот процесс требует большего внимания, но внимательное чтение и программирование приведут вас к результату. В этом разделе мы:

  • Загружаем VGG-19 с помощью API Keras ФЗШ от TensorFlow и загружаем его с весами ImageNet.
  • Создаём матричную функцию Грама для расчёта потери стиля.
  • Выбирам слои обученной модели VGG-19 для контента и стиля.
  • Создаём пользовательскую модель на основе ранее загруженной модели VGG-19 с опцией Keras Model Subclassing.
  • Настраиваем оптимизатор и функции потерь.
  • Определяем настроенный шаг обучения.
  • Запускаем написанный нами цикл обучения.

Обратите внимание на комментарии в gist

Загружаем VGG с Functional API


Поскольку в Keras размещена предварительно обученная модель VGG-19, мы можем загрузить модель из Keras Application API. Сначала создадим функцию, чтобы использовать её позже в разделе Создание подклассов. Эта функция позволяет нам создавать пользовательскую модель VGG с желаемыми слоями, по-прежнему имея доступ к атрибутам модели:


Основная модель с Model Subclassing


Вместо того чтобы сравнивать необработанные промежуточные выходные данные изображения контента и изображения стиля, мы сравним матрицы Грама двух выводов с помощью функции gram_matrix; она даёт результаты точнее:


Модель VGG-19 состоит из 5 блоков со слоями внутри каждого блока, как показано выше. Мы выберем первый свёрточный слой каждого блока, чтобы получить знания о стиле. Поскольку информация промежуточного уровня более ценна для трансферного обучения, мы оставим второй свёрточный слой пятого блока для слоя контента. Следующие строки создают два списка с информацией об этом слое:


Теперь, когда у нас есть выбранные слои, функция gram_matrix() для расчёта потерь и функция vgg_layers() для загрузки желаемого в VGG-19, мы можем создать нашу основную модель с опцией Keras Model Subclassing. С помощью следующих строк мы делаем preprocess_input [предварительно обрабатываем входные] данные, пропуская их через нашу пользовательскую модель VGG и gram_matrix. Cоздаём модель и называем её extractor. Модель выводит словарь, который содержит выходные значения для контента и информации стиля:


Оптимизатор и настройки потерь


Теперь, когда мы можем выводить прогнозы для /информации о/ стиля и содержимого, пришло время настроить оптимизатор нашей модели с помощью Adam и создать пользовательскую функцию потерь:


Пользовательский шаг обучения


Теперь мы определим пользовательскую функцию train_step, в которой воспользуемся преимуществом GradientTape, который, в свою очередь, позволяет выполнять автоматическое дифференцирование для расчёта потерь. GradientTapeзаписывает операции во время прямого прохода, а затем может вычислить градиент нашей функции потерь для входного изображения уже для обратного прохода. Обратите внимание, что мы используем декоратор tf.function(), чтобы TensorFlow знал, что мы передаём функцию trainstep. Свободно экспериментируйте с total_variation_weight, чтобы получить разные результаты переноса стиля.


Настраиваемый цикл обучения


Теперь, когда всё прочитано, мы можем запустить пользовательский цикл обучения, чтобы оптимизировать веса и получить наилучшие результаты. Запустим модель на 20 эпох и 100 steps_per_epoch [шагов на эпоху]. Это даст нам красивую версию фотографии, которую мы загрузили вначале, в стиле поп-арт. Кроме того, наш цикл будет выводить стилизованную фотографию после каждой эпохи (это временно).

Если вы используете Google Colab, чтобы повторить шаги туториала, убедитесь, что вы включили аппаратный ускоритель в настройках блокнота. Это значительно сократит время обучения.


Сохраняем и отображаем стилизованное изображение


Теперь, когда наша модель завершила обучение, мы можем сохранить стилизованную фотографию контента с помощью API предварительной обработки TensorFlow. Следующая строка сохранит фотографию в вашем окружении:


Вот результат:

Рис. 6. Фото истилизованная версия

Поздравляю!


Вы только что построили модель передачи нейронного стиля с помощью трансферного обучения. Очевидно, что есть возможности сделать её лучше, но если вы присмотритесь, то увидите, что наша модель скопировала стиль Уорхола, когда он стилизовал волосы Монро. Модель также позаимствовала цвет фона из диптиха Монро. Поэкспериментируйте с числами img_scale, total_variation_weight, epoch, steps_per_epoch, чтобы получить разные результаты. Вы также можете использовать другие художественные стили, чтобы получить интересные результаты. А если хотите научиться применять машинное обучение иначе приходите учиться, а промокод HABR, дающий 10% дополнительно к скидке на баннере вам в этом поможет.



image



Подробнее..

Краткость сестра таланта Как сделать TransformerSummarizer на Trax

22.02.2021 10:14:04 | Автор: admin

В новой курсеровской специализации NLP от deeplearning.ai в качестве библиотеки глубокого обучения используется Trax. В последнем курсе подробно разбирается механизм внимания и его использование в архитектуре Transformer, в том числе в таких новеллах как BERT и T5. Имея некоторое количество свободного времени специализацию можно пройти за несколько недель, что я собственно и сделал, соблазнившись возможностью построить собственный трансформер. Очень хотелось сделать модель, которая может работать с текстами на русском языке.

Для эксперимента я выбрал саммаризатор, эта конструкция получает на вход статью и генерирует короткий текст с описанием сути. Summary может быть и просто заголовком. Попробую рассказать обо всём в деталях.

Trax полнофункциональная библиотека для глубокого обучения с фокусом на понятный код и быстрые вычисления. По синтаксису она в общем похожа на Keras, а модель на Trax можно сконвертировать в модель на Keras. Библиотека активно развивается и поддерживается командой Google Brain. Trax использует Tensorflow и является одной из библиотек в его экосистеме. Она работает на CPU, GPU и TPU, при этом используется одна и та же версия. Не буду говорить неправду, TPU я пока не попробовал.

Transformer - архитектура глубоких нейронных сетей, представленная в 2017 году исследователями из Google Brain. Transformer предназначен для работы с последовательностями, в том числе текстовыми, но в отличие от архитектур на рекуррентных сетях, не требует обрабатывать последовательность по порядку. Сильно упрощая можно сказать, что если из архитектуры Seq2Seq на LSTM с механизмом внимания оставить только механизм внимания и добавить нейронную сеть прямого распространения (Feed Forward), то он и получится. Подробнее про трансформеры с картинками здесь на английском, здесь на русском.

Данные

В качестве набора данных для эксперимента я решил использовать корпус новостей Lenta.Ru, свежую версию которого нашел на Kaggle. Корпус содержит более 800 тыс. новостных статей в формате (url, title, text, topic, tags, date). Если статья это text, то summary для моей модели title. Это законченное предложение, содержащее основную мысль новостной статьи. Конечно это не полное summary как, например, в англоязычном корпусе cnn_dailymail, но я подумал, что так даже интереснее.

Процесс подготовки данных представлен на схеме:

Для начала я отфильтровал аномально короткие и аномально длинные статьи. Затем выделил из набора тексты и заголовки, преобразовал всё к нижнему регистру, сохранил в виде списка кортежей и в виде полного текста. Список кортежей разбил на две части для обучения (train) и оценки (eval). Далее написал бесконечный генератор, который дойдя до конца списка, перемешивает его и начинает сначала. Неприятно же, когда генератор заканчивается где-то в середине эпохи. Это важно прежде всего для оценочного набора, я взял всего 5% от общего количества статей, примерно 36 тысяч пар.

На основе полного текста я обучил токенайзер, а в качестве токенов использовал части слов. Проблема токенизации или сегментации на целые слова заключается в том, что некоторые слова в тексте встречаются редко, возможно единственный раз, и таких слов очень много, а размер словаря конечен и хочется его сделать не очень большим, чтобы поместиться в память виртуальной машины. Приходится заменять некоторые слова именованными шаблонами, часто использовать заполнитель для слов, которых в словаре нет и даже использовать специальные техники вроде pointer-generator. А разбиение на подслова позволяет сделать токенайзер с небольшим по объему словарем, который еще и работает практически без потерь информации.

Для такой сегментации существует несколько сравнительно честных способов, познакомиться с ними можно например здесь. Я выбрал модель на основе Byte Pair Encoding (BPE), реализованную в библиотеке sentencepiece. BPE способ кодирования текста со сжатием. Для кодирования часто повторяющейся последовательности символов используется символ, которого нет в исходной последовательности. Всё тоже самое и при сегментации, только последовательность часто встречающихся символов становится новым токеном, и так пока не будет достигнут заданный размер словаря. Мой словарь содержит 16000 токенов.

Пример сегментированного текста

['ученые', 'придума', 'ли', 'новый', 'способ', 'взаимо', 'действия', 'с', 'граф', 'ен', 'ом', ',', 'который', 'позволяет', 'избавиться', 'от', '"', 'сли', 'па', 'ющихся', '"', 'ли', 'стов', '.', 'статья', 'ученых', 'появилась', 'в', 'журнале', 'ac', 's', 'n', 'an', 'o', ',', 'а', 'ее', 'крат', 'кое', 'из', 'ложение', 'приво', 'дится', 'на', 'сайте', 'северо', '-', 'запа', 'дного', 'университета', ',', 'сотрудники', 'которого', 'принимали', 'участие', 'в', 'работе', '.']

Видно, что разбиваются даже слова на латинице, а знаки препинания кодируются как отдельные токены, просто мечта, а не токенайзер. Знак нижнего подчеркивания обозначает начало слова.

Обучается модель благодаря вот такой нехитрой конструкции:

import sentencepiece as spmspm.SentencePieceTrainer.train('--input=full_text.txt \                                --pad_id=0 --bos_id=-1 --eos_id=1 --unk_id=2 \                                --model_prefix=bpe --vocab_size=16000 --model_type=bpe')

Результат два файла: словарь для контроля и модель, которую можно загрузить в обертку токенайзера. Для выбранной мной модели статья и заголовок должны быть преобразованы в последовательности целых чисел и объединены с разделением служебными токенами EOS :1 и PAD :0 (конец последовательности и заполнитель).

После преобразования последовательность помещается в корзину фиксированной длинны. У меня их три: 256, 512 и 1024. Последовательности в корзине автоматически дополняются заполнителями до фиксированной длинны и собираются в пакеты (batches). Количество последовательностей в пакете зависит от корзины, соответственно 16, 8, 4.

Рефлексия по поводу последовательностей длиннее 512 токенов

Трудно представить, что 2000 символов могут дать что-то длиннее 512 токенов, но на всякий случай сделал три корзины. А длиннее 1024 не может быть в принципе из-за фильтра в пайплайне.

Сегментация и конкатенация выполняются в пайплайне trax:

input_pipeline = trax.data.Serial(    trax.data.Tokenize(vocab_type='sentencepiece',                       vocab_dir='/content/drive/MyDrive/',                       vocab_file='bpe.model'),    preprocessing,    trax.data.FilterByLength(1024))train_stream = input_pipeline(train_data_stream())eval_stream = input_pipeline(eval_data_stream())

preprocessing это моя функция конкатенации, генератор. Сортировка по корзинам и формирование пакетов осуществляется благодаря следующей конструкции:

boundaries =  [256, 512]batch_sizes = [16, 8, 4]train_batch_stream = trax.data.BucketByLength(    boundaries, batch_sizes)(train_stream)eval_batch_stream = trax.data.BucketByLength(    boundaries, batch_sizes)(eval_stream)

Модель

Transformer, работающий с двумя последовательностями, например при машинном переводе, включает два блока энкодер и декодер, но для саммаризации достаточно только декодера. Такая архитектура в общем реализует языковую модель, где вероятность следующего слова определяется по предыдущим. Еще её называют Decoder-only Transformer и она похожа на GPT (Generative Pre-trained Transformer). Разобраться в деталях архитектур можно здесь.

Для моего случая в библиотеке Trax есть отдельный класс моделей trax.models.transformer.TransformerLM(...), то есть создать модель можно одной строчкой кода. В упомянутой специализации модель строится from scratch. Я же выбрал нечто среднее построил модель из готовых блоков, используя примеры кода.

Схема модели показана на рисунке:

PositionlEncoder() это блок, обеспечивающий построение векторного пространства и кодирование позиции токена во входной последовательности. Код:

from trax import layers as tldef PositionalEncoder(vocab_size, d_model, dropout, max_len, mode):    return [         tl.Embedding(vocab_size, d_model),          tl.Dropout(rate=dropout, mode=mode),         tl.PositionalEncoding(max_len=max_len, mode=mode)] 

Аргументы:
vocab_size (int): размер словаря
d_model (int): количество признаков векторного пространства
dropout (float): степень использования dropout
max_len (int): максимальная длина последовательности для позиционного кодирования
mode (str): 'train' или 'eval' для dropout и поз. кодирования.

FeedForward формирует блок прямого распространения с выбранной функций активации:

def FeedForward(d_model, d_ff, dropout, mode, ff_activation):    return [         tl.LayerNorm(),         tl.Dense(d_ff),         ff_activation(),        tl.Dropout(rate=dropout, mode=mode),         tl.Dense(d_model),         tl.Dropout(rate=dropout, mode=mode)     ]

Аргументы:
d_model (int): количество признаков векторного пространства
d_ff (int): ширина блока или количество юнитов в выходном плотном слое
dropout (float): степень использования dropout
mode (str): 'train' или 'eval' чтобы не использовать dropout при оценке качества модели
ff_activation (function): функция активации, в моей модели ReLU

DecoderBlock(...) - это два блока с Residual-соединием. Вряд ли перевод остаточный точно отражает смысл, но это обходное соединение для борьбы с исчезающим градиентом в глубоких архитектурах.

Если считать от входа к выходу, то первый блок содержит механизм внимания, я использовал готовый уровень из библиотеки. Второй описанный выше блок прямого распространения. Механизм внимания здесь необычный, он смотрит на ту же последовательность, для которой генерируется следующий токен, а чтобы он не заглядывал в будущее при расчете весов используется специальная маска.

def DecoderBlock(d_model, d_ff, n_heads, dropout, mode, ff_activation):            return [      tl.Residual(          tl.LayerNorm(),           tl.CausalAttention(d_model, n_heads=n_heads, dropout=dropout, mode=mode)         ),      tl.Residual(          FeedForward(d_model, d_ff, dropout, mode, ff_activation)        ),      ]

Из неизвестных аргументов только n_heads (int) количество головок внимания, надеюсь это удачный термин для attention heads. Каждая головка учится обращать внимание на что-то своё.

Собираю все части вместе и задаю параметры модели. У меня шесть декодеров, в каждом из которых по восемь головок внимания. Общее количество обучаемых параметров 37 412 480.

Из неизвестных мне уровней пожалуй только ShiftRight. Он сдвигает входную последовательность вправо, заполняя освободившееся место нулями, по умолчанию на одну позицию. Это нужно для teacher forcing, специальной техники, упрощающей обучение языковой модели, особенно на ранних этапах. Идея здесь в следующем: когда модель учится прогнозировать следующее слово по предыдущим, вместо прогноза модели, возможно неверного, в качестве этих предыдущих слов используются правильные ответы (ground truth). Коротко это можно описать формулой:
y(t) = x(t+1). Здесь подробное объяснение для RNN.

def SumTransformer(vocab_size=vocab_size,                  d_model=512,                  d_ff=2048,                  n_layers=6,                  n_heads=8,                  dropout=0.1,                  max_len=4096,                  mode='train',                  ff_activation=tl.Relu):    decoder_blocks = [DecoderBlock(d_model, d_ff, n_heads, dropout, mode,                       ff_activation) for _ in range(n_layers)]     return tl.Serial(        tl.ShiftRight(mode=mode),         PositionalEncoder(vocab_size, d_model, dropout, max_len, mode),        decoder_blocks,         tl.LayerNorm(),         tl.Dense(vocab_size),         tl.LogSoftmax()     )

Обучение

По моему опыту Google Colab не очень любит длительное использование своих GPU и не всегда их выделяет, особенно во второй половине дня. Поэтому я обучал модель отдельными эпохами по 20 000 шагов, где шаг соответствует одному пакету (batch). Получалось сделать 1-2 эпохи в день. 100 шагов это примерно минута, а эпоха около трех часов.

Первая эпоха показала, что модель учится только несколько тысяч шагов, дальше никаких улучшений не происходит. Оказалось, что я выбрал слишком большой шаг обучения (learning_rate). Для моей модели он должен быть 0.0002 первые несколько эпох, затем 0.0001 и 0.00005 в конце. Если бы я учил модель за один проход, то можно было бы использовать lr_schedules из trax.supervised. Там есть разные удобные варианты и с прогревом и с постепенным уменьшением шага.

В качестве метрик я использовал CrossEntropyLoss и Accuracy. За 12 эпох на оценочном наборе loss упал с 10 до 2, а доля правильных ответов возросла почти до 60%. Этого оказалось достаточно, чтобы генерировать почти приемлемые заголовки.

Цикл обучения выглядит следующим образом:

from trax.supervised import trainingdef training_loop(SumTransformer, train_gen, eval_gen, output_dir = "~/model"):    output_dir = os.path.expanduser(output_dir)    train_task = training.TrainTask(         labeled_data=train_gen,        loss_layer=tl.CrossEntropyLoss(),        optimizer=trax.optimizers.Adam(0.0001),        n_steps_per_checkpoint=100    )    eval_task = training.EvalTask(         labeled_data=eval_gen,         metrics=[tl.CrossEntropyLoss(), tl.Accuracy()]     )    loop = training.Loop(SumTransformer(),                         train_task,                         eval_tasks=[eval_task],                         output_dir=output_dir)        return loop

Аргументы:
SumTransformer (trax.layers.combinators.Serial): модель
train_gen (generator): поток данных для обучения
eval_gen (generator): поток данных для оценки качества.
output_dir (str): папка для файла модели, откуда её можно скопировать на Google Drive перед выключением виртуальной машины.

Дальше всё просто:

loop = training_loop(SumTransformer, train_batch_stream, eval_batch_stream)loop.run(20000)

и три часа ожидания...

Оценка результатов

Для оценки результатов я использовал жадный декодер на базе argmax, который определяет индекс наиболее вероятного токена в словаре по положению максимального значения в выходном тензоре. Далее токен добавляется к входной последовательности и операция повторяется пока не появится символ EOS или не будет достигнута заданная максимальная длина предложения.

Примеры из оценочного набора:
(Исходный текст сокращен)

Тест #1: швейцарская часовая компания audemars piguet представила новую модель из коллекции royal oak. как сообщает luxurylaunches, речь идет о часах с вечным календарем. официальная презентация пройдет в рамках международного салона высокого часового искусства sihh, который проходит в женеве...
Образец: дом audemars piguet оснастил часы вечным календарем
Модель: audemars piguet представила новую модель из коллекции royal oak

Тест #2: на ежегодном фестивале в городе грэхэмстаун, юар, фокусник случайно выстрелил в голову своему напарнику во время представления. об этом сообщает местная газета the daily dispatch. инцидент произошел 30 июня. брендон пил (brendon peel) и его ассистент ли лау (li lau) выполняли магический трюк перед многочисленной аудиторией, когда пил по неосторожности пустил в затылок напарника стрелу...
Образец: фокусник случайно подстрелил ассистента наглазах узрителей
Модель: на фестивале в грэлково напали с ножом
(И не в грэлково, и не напали, и не с ножом, но спасибо, что это было холодное оружие, а не пистолет)

Еще примеры

Тест #3: международный валютный фонд (мвф) в среду, 15 мая, утвердил выделение кипру кредита в размере 1,33 миллиарда долларов (миллиард евро). как сообщает agence france-presse, в качестве первого транша кипрское правительство получит 110,7 миллиона долларов. утвержденный 15 мая кредит является частью плана помощи...
Образец: мвф выделил кипру миллиард евро
Модель: мвф утвердил кредит на кипрский кредит

Тест #4: автопортрет энди уорхола, выполненный в 1965 году и ранее не выставлявшийся, продадут с аукциона, пишет the new york times. автопортрет более 40 лет хранила бывшая секретарша уорхола кэти нейсо (cathy naso), которая получила картину от художника в оплату ее работы. нейсо работала в студии уорхола...
Образец: неизвестный автопортрет энди уорхола выставят наторги
Модель: энди уорхола продадут с аукциона

Тест #5: sony решила выпустить файтинг, который станет "ответом на игру super smash bros" от nintendo, пишет vg24/7 со ссылкой на paul gale network и neogaf. в новом проекте, в настоящее время известном под названием title fight, герои из нескольких игр издательства сразятся между собой...
Образец: sony приписали разработку нового файтинга
Модель: sony выпустит файтинг от nintendo

Интересно, что на ранних этапах обучения вместо белиберды модель генерирует почти осмысленные фейки. Чтобы посмотреть как это происходит, я сделал скринкаст нескольких интересных на мой взгляд вариантов:

Ссылки

  • Мой репозитарий с кодом эксперимента)

  • Репозитарий trax

  • Математика механизма внимания в знаменитой статье Attention Is All You Need. Кстати один из авторов статьи, Lukasz Kaiser, штатный исследователь Google Brain, является также автором и инструктором специализации.

Примечания

Я использовал trax 1.3.7, он инсталлируется через pip, но не работает под Windows. На форумах пишут что можно под WSL. А еще там нет beam_search, который есть в документации и который я очень хотел попробовать.

Параметры для модели взяты из заведомо работающей модели для cnn_dailymail. С учетом более коротких последовательностей, предполагаю, что размер плотного слоя в блоке FeedForward и максимальную длину последовательности можно уменьшить. Вопрос эксперимента.

В упомянутой модели TransformerLM выход не нормализован (нет уровня softmax).

Подробнее..

Нейродайджест главное из области машинного обучения за февраль 2021

01.03.2021 20:22:05 | Автор: admin

Как вы знаете, в подборку мы всегда включаем самые интересные публикации на тему машинного обучения, и приоритет отдается проектам с непустыми репозиториями. Так вот, февраль порадовал в этом плане рядом сервисов, поэтому с них и начнем. Поехали:


Papers with Datasets and Libraries


Есть такой ресурс Papers with Code, миссия которого прямо соответствует названию агрегировать публикации из сферы машинного обучения, у которых есть код, а также дать возможность предложить свою имплементацию.

В этом месяце они запустили раздел с доступными наборами данных, в котором уже проиндексировано больше 3000 исследовательских датасетов. В каталоге можно искать датасеты по частоте упоминаний, сфере применения, типу данных и поддерживаемому языку.

Помимо этого они добавили возможность искать предварительно обученные модели классификации изображений, которые можно файнтюнить на ваших собственных наборах данных. На данный момент их уже 300+, и каталог продолжит пополняться.

Google Model Search


Доступность: страница проекта, репозиторий

Успех нейронной сети часто зависит от того, насколько широко ее можно применять для различных задач. При создании модели приходится принимать ряд сложных архитектурных решений какой глубины должна быть нейросеть, какие типы слоев в ней использовать и т.д.

Google представили платформу, которая поможет находить подходящую архитектуру, соответствующую вашему датасету и задаче, что сократит время на конфигурацию и написание кода и потребует меньше вычислительных ресурсов.

Библиотека позволяет запускать алгоритмы из коробки на ваших данных независимо от предметной области автоматически подбирать оптимальную архитектуру, правильные ансамбли моделей или дистиллированные модели.

ZenML


Доступность: сайт проекта / репозиторий

MLOps фреймворк, который упрощает перенос пайплайнов из ноутбуков в продакшн-среду. Гарантированная воспроизводимость обучающих экспериментов за счет версионирования данных, кода и моделей. Платформа также позволяет быстро переключаться между локальной и облачной средой, предоставляет готовые хэлперы для сравнения и визуализации параметров и результатов, кеширования состояний конвейера для быстрых итераций и многое другое.

TensorFlow 3D


Доступность: Статья / репозиторий

С распространением устройств, захватывающих 3D-данные, вроде лидаров и камер измерения глубины, обострилась потребность в технологии обработки этих данных и понимания трехмерной сцены. Это нужно для навигации и работы в реальном мире самоуправляемых автомобилей и роботов, а также для совершенствования AR-технологий.

Google представили модульную библиотеку для применения глубокого обучения на 3D-данных в TensorFlow. Она содержит пайплайны обучения и оценки для трехмерной семантической сегментации, классификации сцены, обнаружения трехмерных объектов и т.д.

MeInGame



Доступность: статья / репозиторий

В компьютерных играх часто есть редактор персонажа, который позволяет с помощью настроек разных параметров менять внешность игрока. Алгоритм MeInGame позволяет создать кастомного персонажа всего по одной фотографии. Нейросеть предсказывает форму лица и его текстуру. Хотя методы, основанные на 3D Morphable Face Model (3DMM), могут генерировать 3D-портрет из отдельных изображений, топология сетки обычно отличается от тех, что используются в большинстве игр. Авторы этого алгоритма заявляют, что эту проблему решили.

SAM



Доступность: статья / репозиторий

Правдоподобное моделирование старения по одной фотографии лица чрезвычайно сложная задача, так как нужно моделировать изменение отдельных лицевых черт и даже формы головы, сохраняя при этом идентичность человека.

Внутри используется StyleGAN, но здесь исследователи также используют предварительно обученную сеть возрастной регрессии, с помощью которой кодировщик генерирует скрытые коды, соответствующие целевому возрасту. Метод рассматривает процесс непрерывного старения как задачу регрессии между входным возрастом и целевым возрастом, обеспечивая точный контроль над созданным изображением. Модель позволяет редактировать сгенерированные изображения.

VOGUE



Доступность: страница проекта / интерактивное демо

Новый кейс применения StyleGAN для виртуальной примерки одежды. Алгоритм переносит одежду с фотографии одного человека на фотографию человека, которая подается на вход. В основе метода лежит интерполяция скрытого пространства с учетом позы StyleGAN2, которая работает с формой тела, волосами, цветом кожи целевого человека. Алгоритм позволяет одежде деформироваться в соответствии с заданной формой тела, сохраняя при этом узор и детали материала. На выходе получается фотореалистичные изображения в достойном разрешении 512x512.

NeRViS



Доступность: страница проекта / репозиторий

Существующие методы стабилизации видео либо сильно обрезают границы кадра, либо создают артефакты и искажения. Данный алгоритм
предварительно оценивает плотные поля деформации и для синтеза полного стабилизированного кадра использует соседние кадры. Новизна подхода в основанном на обучении гибридном пространственном синтезе, который устраняет артефакты, вызванные неточностью оптического потока и быстро движущимися объектами.

Stable View Synthesis


Доступность: статья / репозиторий

На основе набора фотографий, изображающих сцену со свободно распределенных точек обзора, алгоритм синтезирует новые виды сцены. Метод работает на геометрическом скаффолдинге, который расчитывается на основе SfM-фотограмметрии. Целевое представление рендерится сверточной сетью из тензора характеристик, синтезированных для всех пикселей.

Статью опубликовали еще в ноябре прошлого года, но код стал доступен только сейчас.

JigsawGan


Доступность: статья

Генеративная self-supervised нейросеть, обученная собирать пазлы. На вход модель принимает беспорядочно расположенные части изображения и без подсказок восстанавливает из них оригинальное изображение, то есть, модель не знает, каким было изображение изначально.

CharacterGAN



Доступность: статья / репозиторий

Генеративная нейросеть, которую можно обучить только на нескольких изображениях персонажа в разных позах, для генерации новых поз по расположению ключевых точек. Это позволяет анимировать статичные изображения. Новизна подхода в том, что изображение разбивается на слои, каждый из которых обрабатывается отдельно. Это решает проблему преграждений, когда на передний план выходит посторонний объект. Для удобства добавлен GUI, позволяющий вручную корректировать позы по ключевым точкам.

Discrete VAE


Доступность: репозиторий

В прошлом выпуске мы рассказывали о потрясающей DALL-E. В конце февраля OpenAI создали репозиторий с названием модели, но саму модель пока что не выложили внутри только часть модели, а именно PyTorch пакет для discrete VAE. Это вариационный автоэнкодер, который, в нашем случае, генерирует изображения из текстовых описаний.

Deep Nostalgia


Доступность: онлайн-сервис

Ну и напоследок, всегда приятно когда на основе моделей делают простой и понятный продукт. Так вот, компания MyHeritage, которая занимается вопросами генеалогии и родословных, судя по всему, взяла алгоритм First Order Model, прикрутила удобный пользовательский интерфейс и сделала на его основе сервис по оживлению фотографий.

В результате тонны сгенерированного пользовательского контета и огромные виральные охваты. А еще говорят, что бизнесу ИИ ни к чему.

На этом все, спасибо за внимание и до встречи через месяц!
Подробнее..

Применение предобученной модели VGG16 для рекомендаций на основе изображений товаров

04.03.2021 02:16:58 | Автор: admin

Сегодня я хочу рассказать вам о своем опыте использования нейронной сети для поиска похожих товаров для рекомендательной системы интернет-магазина. Говорить буду в основном о технических вещах. Написать эту статью на Хабре решил потому, что когда только начинал делать этот проект, то на Хабре нашел одно подходящее решение, но как оказалось, оно уже было устаревшим и пришлось его модифицировать. А поэтому решил обновить материал для тех, у кого будет потребность в аналогичном решении.


Отдельно хочу сказать, что это мой первый опыт создания более-менее серьезного проекта в сфере Data Science, поэтому если кто-то из более опытных коллег увидит, что еще можно улучшить, то буду только рад за советы.

Начну с небольшой предистории, почему была выбрана та логика интернет-магазина, которая выбрана - а именно рекомендация на основе похожих товаров (а не методы коллаборативной фильтрации, например). Дело в том, что данная рекомендательная система разрабатывалась для интернет-магазина, который продает часы и поэтому до 90% пользователей, которые приходят на сайт, они больше не возвращаются. А в целом задача была такая - увеличить количество просмотров страниц со стороны пользователей, которые приходят на страницы конкретных товаров по рекламе. Такие пользователи просматривали одну страницу и уходили с сайта, если товар им не подходил.

Надо сказать, что в данном проекте у меня не было возможности делать интеграцию с бэкэндом интернет-магазина - классическая история для малых и средних интернет-магазинов. Необходимо было рассчитывать только на систему, которую я сделаю в стороне от сайта. Поэтому в качестве визуального решения на самом сайте я решил сделать всплывающий js-виджет. Одной строчкой в html-код добавляется js, понимает заголовок страницы, на который пришел пользователь, и передает его в бэкэнд сервиса. Если бэкэнд нашел в своей базе заранее загруженных товаров товар, то он ищет опять же в заранее подготовленной базе товаров, рекомендации и возвращает их в js, а js их потом отображает в виджете. Также, для снижения влияния на скорость загрузки, js создает iframe, в котором производит все работы с отображением виджета. Помимо прочего, это еще и позволяет убрать проблему с пересечением css-классов виджета и сайта.

Скорее всего, для это была самая не интересная часть для Data Science. Но считаю ее необходимой частью данного рассказа, чтобы более четко понимать как все работает. Опять же, возможно, кому-то из новичков это тоже будет полезно.

А теперь перейдем непосредственно к поиску похожих товаров.

Для данного магазина я сделал два варианта поиска схожих товаров (опять же, классика A/B-тестирования) - рекомендации просто по схожим характеристикам; рекомендации, которые включали в себя первым слоем схожесть изображения, а вторым - схожесть характеристик.

С поиском схожих просто по характеристикам дело ясное. Перейдем к поиску схожих по изображениям.

Для начала подгружаем необходимые библиотеки:

!pip install theano%matplotlib inlinefrom keras.models import Sequentialfrom keras.layers.core import Flatten, Dense, Dropoutfrom keras.layers.convolutional import Convolution2D, MaxPooling2D, ZeroPadding2Dfrom keras.optimizers import SGDimport cv2, numpy as npimport osimport h5pyfrom matplotlib import pyplot as pltfrom keras.applications import vgg16from keras.applications import Xceptionfrom keras.preprocessing.image import load_img,img_to_arrayfrom keras.models import Modelfrom keras.applications.imagenet_utils import preprocess_inputfrom PIL import Imageimport osimport matplotlib.pyplot as pltimport numpy as npfrom sklearn.metrics.pairwise import cosine_similarityimport pandas as pdimport theanotheano.config.openmp = True

Дальше мне необходимо отсортировать изображения (так как они у меня названы исходя из индексов товаров в датафрейме, то я их отсортирую по алфавиту):

import redef sorted_alphanumeric(data):    convert = lambda text: int(text) if text.isdigit() else text.lower()    alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ]     return sorted(data, key=alphanum_key)dirlist = sorted_alphanumeric(os.listdir('images'))r1 = []r2 = []for i,x in enumerate(dirlist):    if x.endswith(".jpg"):        r1.append((int(x[:-4]),i))        r2.append((i,int(x[:-4])))extid_to_intid_dict = dict(r1)intid_to_extid_dict = dict(r2)

Задаем некоторые параметры:

imgs_path = "images/"imgs_model_width, imgs_model_height = 224, 224nb_closest_images = 3 # количество ближайших изображений (для тестирования вывода)

Загружаем саму модель (уже предобученную):

vgg_model = vgg16.VGG16(weights='imagenet')

Удаляем последний слой (следний слой даюет на выходе вероятности каждого из 1000 классов ImageNet - нам это не нужно в данном случае. Нам нужен просто 4096-мерный вектор для каждого из изображений, по которым мы потом будем искать с помощью простой косинусной метрикой).

Как найти имя слоя это отдельный анекдот, поэтому я смотрел в исходники модели.

Итак:

feat_extractor = Model(inputs=vgg_model.input, outputs=vgg_model.get_layer("fc2").output)

Если кому интересно, можно вывести на экран список слоев CNN и посмотреть. Но в целом это никак не относится к коду, который ищет схожие изображения:

feat_extractor.summary()

Далее я иду в каталог заранее подготовленных изображений товаров (то есть, я беру xml каталога интернет-магазина, прохожусь по урлам, которые нашел для товаров, и скачиваю их в папку; ниже покажу код, который делает эту работу):

files = [imgs_path + x for x in os.listdir(imgs_path) if "jpg" in x]print("number of images:",len(files))

Дальше мне необходимо отсортировать изображения по названиям для более аккуратной работы с ними в последующем:

import redef atof(text):    try:        retval = float(text)    except ValueError:        retval = text    return retvaldef natural_keys(text):    '''    alist.sort(key=natural_keys) sorts in human order    http://nedbatchelder.com/blog/200712/human_sorting.html    (See Toothy's implementation in the comments)    float regex comes from https://stackoverflow.com/a/12643073/190597    '''    return [ atof(c) for c in re.split(r'[+-]?([0-9]+(?:[.][0-9]*)?|[.][0-9]+)', text) ]files.sort(key=natural_keys)

Далее загружаю изображения в специальный PIL формат:

original = load_img(files[1], target_size=(imgs_model_width, imgs_model_height))plt.imshow(original)plt.show()print("image loaded successfully!")

Конвертирую PIL изображения в numpy array:
в PIL формат данных - width, height, channel
в Numpy - height, width, channel

numpy_image = img_to_array(original) # сырое изображения в вектор

Конвертирую изображения в batch format.
expand_dims добавит дополнительное измерение к данным на определенной оси

Мы хотим, чтобы входная матрица в сеть имела следующий формат - batchsize, height, width, channels. Таким образом, мы добавляем дополнительное измерение к оси 0.

image_batch = np.expand_dims(numpy_image, axis=0) # превращаем в вектор-строку (2-dims)print('image batch size', image_batch.shape)

Подготавливаем изображение для VGG:

processed_image = preprocess_input(image_batch.copy()) #  библиотечная подготовка изображения

Теперь нам необходимо получить как бы особенности данного вектора (вытащить признаки):

img_features = feat_extractor.predict(processed_image)

То есть это будет векторочек признаков для данного изображения:

print("features successfully extracted!")print("number of image features:",img_features.size)img_features

Теперь всё тайное стало явью, и осталось дело техники итерироваться по всем изображениям и для каждого из него произвести векторизацию.

importedImages = []for f in files:    filename = f    original = load_img(filename, target_size=(224, 224))    numpy_image = img_to_array(original)    image_batch = np.expand_dims(numpy_image, axis=0)        importedImages.append(image_batch)    images = np.vstack(importedImages)processed_imgs = preprocess_input(images.copy())

Вытащим все особенности изображения:

imgs_features = feat_extractor.predict(processed_imgs)print("features successfully extracted!")imgs_features.shape

Ну и дальше посчитаем уже косинусную схожесть между изображениями:

cosSimilarities = cosine_similarity(imgs_features)

Сохраним результат в pandas dataframe:

columns_name = re.findall(r'[0-9]+', str(files))cos_similarities_df = pd.DataFrame(cosSimilarities, columns=files, index=files)cos_similarities_df.head()

Дальше получилась такая ситуация. В каталоге товаров данного магазина около 6000 SKU. После всех манипуляций выше получилась матрица размером 6000 * 6000. В каждом пересечении строк и столбцов находилось число формата float от 0 до 1 с 8 знаками после нуля, которое показывало схожесть. Когда я пробовал сохранить эту матрицу как есть в файл для дальнейшего применения в сервисе рекомендаций, то у меня получался файл весом около 430 мегабайт (хотя в оперативной памяти такая матрица занимала около 130 мегайбайт). Для меня это было не приемлемо. Хотя бы по той простой причине, что мне нужно было как-то выкладывать этот файл в GitHub, а дальше автоматома деплоить на сервер. GitHub не позволяет грузить файлы больше 100 мегайбат (или во всяком случае я не знаю как это делать). Да и в целом мне казалось, что это какой-то неприлично большой файл. Поэтому я начал думать :) И придумал вот что - мне по-большому счету не важно сколько знаков после запятой будет здесь - мне главное просто сравнивать цифры между собой. Поэтому я сделал следующее:

cos_similarities_df_2.round(2) # cos_similarities_df_2 - название датафрейма с косинусными метриками, которые сохранил выше

То есть, для начала я просто взял и отсек лишние цифры. Но формат колонки все равно оставался float. А в pandas float может быть минимально float16 - много.

Тогда я решил перевести эти значения в int:

cos_similarities_df_2.apply(lambda x: x * 100)cos_similarities_df_2.apply(lambda x: x.astype(np.uint8))

После этих манипуляций размер матрицы в оперативке уменьшился до 31 мегабайта. Это уже радовало.

Ну и дальше я сохранил этот файл в h5:

cos_similarities_df_2.to_hdf('storage/cos_similarities.h5', 'data')

В итоге получился файл весом 40 мегбайт. Это уже во-первых, удовлетворяло моим требованиям для хранения в GitHub, а во-вторых, в целом уже не так пугало своим размером :)

Ну и дальше если для проверки себя вы хотите посмотреть, что же там нашлось из похожего, то можно сделать так:

import re# function to retrieve the most similar products for a given onedef retrieve_most_similar_products(given_img):    print("-----------------------------------------------------------------------")    print("original product:")    original = load_img(given_img, target_size=(imgs_model_width, imgs_model_height))    original_img = int(re.findall(r'[0-9]+', given_img)[0])    print((df_items_2.iloc[[original_img]]['name'].iat[0], df_items_2.iloc[[original_img]]['pricer_uah'].iat[0], df_items_2.iloc[[original_img]]['url'].iat[0]))       plt.imshow(original)    plt.show()    print("-----------------------------------------------------------------------")    print("most similar products:")    closest_imgs = cos_similarities_df[given_img].sort_values(ascending=False)[1:nb_closest_images+1].index    closest_imgs_scores = cos_similarities_df[given_img].sort_values(ascending=False)[1:nb_closest_images+1]    for i in range(0,len(closest_imgs)):        original = load_img(closest_imgs[i], target_size=(imgs_model_width, imgs_model_height))        item = int(re.findall(r'[0-9]+', closest_imgs[i])[0])        print(item)        print((df_items_2.iloc[[item]]['name'].iat[0], df_items_2.iloc[[item]]['pricer_uah'].iat[0], df_items_2.iloc[[item]]['url'].iat[0]))        plt.imshow(original)        plt.show()        print("similarity score : ",closest_imgs_scores[i])kbr = '' # напишите сюда название товараfind_rec = int(df_items_2.index[df_items_2['name'] == kbr].tolist()[0]) # df_items_2 название моего базового датафрейма, куда я скачал каталог товаровprint(find_rec)retrieve_most_similar_products(files[find_rec])

Вот и все :)

Ну и дальше можно повесть файл со скриптами на крон и автоматизировать работу по регулярному переполучению новых рекомендаций при обновлении каталога.

А теперь еще покажу как я спарсил изображения, если это кому-то будет полезно:

Для начала создам необходимые мне директории, куда все буду складывать:

import osif not os.path.exists('storage'):    os.makedirs('storage')if not os.path.exists('images'):    os.makedirs('images')

Не буду показывать код, который делает подготовительну работу по созданию данных из xml магазина - это просто и не интересно.

А код, который забирает изображения, вот:

# importing required modulesimport urllib.requestimage_counter = 0error_list = []# Функция для загрузки изображения в нужную мне директориюdef image_from_df(row):    global image_counter        item_id = image_counter        filename = f'images/{item_id}.jpg'    image_url = f'{row.image}'    try:      conn = urllib.request.urlopen(image_url)           except urllib.error.HTTPError as e:      # Return code error (e.g. 404, 501, ...)      error_list.append(item_id)    except urllib.error.URLError as e:      # Not an HTTP-specific error (e.g. connection refused)            print('URLError: {}'.format(e.reason))    else:      # 200      urllib.request.urlretrieve(image_url, filename)      image_counter += 1

Теперь я беру мой сформированный датасет товаров из xml, и начинаю идти по каждой строчке и забирать изображения:

df_items_2.apply(lambda row: image_from_df(row), axis=1)

Попадаются товары, у которых нет изображений. Тогда я их просто удаляю из датафрейма и не буду для них делать рекомендации. Обратите на это внимание в своих работах. Я попался на эту тему, так как сначала мне отдавали xml с товарами, которые все имели изображения. А когда после очередного запуска скрипта он упал, то потратил пару часов, чтобы понять, что не все товары имеют изображения и пришлось уже писать как положено для тех случаев, когда не уверен в том, что данные прийдут как положено.

for i in error_list:  df_items_2.drop(df_items_2.index[i], inplace = True)  df_items_2.reset_index(drop=True, inplace = True) print(f'Удалил строки без изображений: {error_list}')print(len(error_list))

Собственно говоря, вот и все. Надеюсь, это кому-то будет полезно! )

А если нет, то не судите строго - хотел как лучше )

P.S. Кстати, недавно открыл для себя следующую вариацию VGG - VGG19. Судя по тестам, эта версия дает еще более лучшие предсказания.

P.S.S Отдельно хочу выразить большую благодарсть людям, без которого я бы не смог все это реализовать: это мой брат, Senior JavaScript Developer (помогал мне написать js для сайта и обходить CORS-политики); это Костя Дедищев, Senior Python Developer и Senior Engineer (помогал мне заворачивать все в Docker и настраивать CI/CD pipeline); это Екатерина Артюгина из SkillFactory, которая три месяца возилась со мной и с другими ребятами с рамках SkillFactory Accelerator (это курс я взял специально для того, чтобы создать свой первый реальный Data Science проект под присмотром более опытных ребят); это и Валентина Бабушкина (ментор, который помогал понять суть A/B-тестов и включить их в проект); это Валентин Малых (еще один ментор, который помогал с разумением NLP проблем и в частности работы опечаточников при создании чат-ботов (еще второй проект, над которым я работал в рамках акселлератора и о котором я может быть расскажу чуть позже); это Эмиль Маггерамов (ментор, который в целом курировал мое продвижение в акселлераторе по созданию данного проекта); это одногруппники Valery Kuryshev и Георгий Брегман (регулярно раз в неделю созванивались и делились полученным за неделю опытом).

Подробнее..

Нейродайджест главное из области машинного обучения за май 2021

03.06.2021 14:23:54 | Автор: admin

Управляемые складки одежды и морщины, фотореалистичные симуляции вождения, естественное освещение объектов при смене фона, китайский аналог DALL-E и многое другое: встречайте подборку самых интересных исследований и нейросетевых моделей, которые появились в прошедшем месяце.

DECA

Доступность: страница проекта / статья / репозиторий

Современные методы монокулярной трехмерной реконструкции создают лица, которые невозможно реалистично анимировать, поскольку они не моделируют изменение морщин в зависимости от выражения. Также модели, обученные на высококачественных сканированных изображениях, плохо работают на фото, сделанных в естественных условиях.

Данный подход регрессирует трехмерную форму лица и анимируемые черты лица, которые меняются в зависимости от артикуляции. Модель обучена создавать карту UV-смещений из низкоразмерного скрытого представления, которое состоит из специфичных для человека параметров, а регрессор обучен предсказывать параметры формы, позы и освещения из одного изображения. Для этого авторы создали функцию потерь, которая отделяет индивидуальные особенности лица от морщин, которые зависят от артикуляции. Такое разделение позволяет синтезировать реалистичные морщины, характерные для конкретного человека, и управлять параметрами выражения лица, сохраняя при этом индивидуальные особенности человека.

Garment Collision Handling

Доступность: страница проекта / статья

Симуляция деформации и движения одежды на человеке часто приводит к тому, что текстура одежды проникает внутрь модели тела. Существующие методы виртуальной примерки требуют этап постобработки, чтобы устранить этот нежелательный эффект. Данный подход напрямую выводит трехмерные конфигурации одежды, которые не пересекаются с телом.

Модель симулирует деформацию одежды и реалистичное движение складок в зависимости от изменения позы. Достигается это с помощью новогоканонического пространства для одежды, которое устраняет зафиксированные диффузной моделью человеческого тела деформации позы и формы, которая и экстраполирует свойства поверхности тела, такие как скиннинг и блендшейп, на любую трехмерную точку.

DriveGAN

Доступность: страница проекта / статья

Для автопилотов и реалистичных тренажеров нужны данные, которые приходится собирать вручную, а это очень долгий и трудоемкий процесс. Можно использовать машинное обучение, чтобы стимулировать ответную реакцию среды на действия непосредственно из данных. Исследователи из NVIDIA и MIT обучили нейросеть на сотнях часов дорожных видео, чтобы моделировать динамическую среду непосредственно в пиксельном пространстве на основе неразмеченных последовательностей кадров и связанных с ними действий.

В получающихся синтезируемых дорожных путешествиях можно моделировать погодные условия, время суток и расположение объектов. Симуляцией можно управлять через графический интерфейс с помощью поворотов руля и изменения скорости.

Enhancing Photorealism Enhancement

Доступность: страница проекта / статья / репозиторий

Пока приходится ждать симулятор езды от NVIDIA, можно развлечь себя с помощью разработок от исследователей из Intel. Они представили подход к повышению реалистичности синтетических изображений. Сверточная нейросеть использует промежуточные представления, созданные обычными пайплайнами рендеринга, что позволяет добиться фотореалистичной картинки в GTA V.

CogView

Доступность: онлайн-демо / статья / репозиторий

Новая нейросеть для перевода текста в изображение. В основе модели трансформер на 4 миллиарда параметров и токенизатор VQ-VAE. Создатели утверждают, что их модель работает лучше DALL-E от OpenAI, и в статье также делятся подходом к файнтюнингу модели для решения других задач вроде обучения стилю, улучшению разрешения, а также стабилизации предварительного обучения.

Попробовать модель можно уже сейчас, правда онлайн-демо пока понимает только текст на китайском.

Expire-Span

Доступность: публикация в блоге / статья / репозиторий

В отличие от человеческой памяти, большинство нейронных сетей обрабатывают информацию целиком, без разбора. При небольших масштабах это не вызывает проблем. Но современные крупные модели, которые на вход принимают полноценные книги или десятки часов видеозаписей, требуют все больше вычислительные мощностей.

Исследователи из FAIR решили научить модели забывать информацию, чтобы фокусироваться только на том, что имеет значение. Сначала модель предугадывает информацию, которая наиболее актуальна для поставленной задачи. В зависимости от контекста, данным присваивается дата истечения срока действия, с наступлением которой информация выбрасывается моделью.

Wav2Vec-U

Доступность: публикация в блоге / статья / репозиторий

Есть много моделей распознавания речи, которые превосходно справляются с распространенными языками. Но множество диалектов все еще не поддерживаются этими технологиями. Это связано с тем, что высококачественные системы необходимо обучать с использованием большого количества размещенных аудиозаписей. Исследователи FAIR представили версию модели wav2vec-U, которая обучается без учителя и вообще не требуют размеченных данных.

Rethinking Style Transfer

Доступность: страница проекта / статья / репозиторий

Существует много качественных моделей для переноса стиля. В большинстве из них процесс стилизации ограничен оптимизацией пикселей. Это не совсем естественно, так как картины состоят из мазков кисти, а не пикселей. Данный метод предлагает стилизовать изображения путем оптимизации параметризованных мазков кисти и дифференцируемого рендеринга. Этот подход улучшает визуальное качество и обеспечивает дополнительный контроль над процессом стилизации пользователь может управлять потоком мазков.

Relit

Доступность: страница проекта / статья

Когда вы сидите перед телевизором или монитором, ваше лицо активно освещается изменяющимся потоком света с экрана. Исследователи обучили нейронную сеть, которая принимает на вход фото лица и текущую картинку на мониторе и предсказывает, как будет выглядеть лицо в таком освещении с монитора. Таким образом, можно контролировать виртуальное освещение лица для видео с вебкамеры. Можно предстать перед коллегами в выгодном свете при очередном видеосозвоне.

Total Relighting

Доступность: страница проекта / статья

Исследователи из GoogleAI пошли дальше и представили систему, которая способна заменить фон фотографии и скорректировать освещение человека на ней, сохраняя четкими границы объектов. На вход подаются две фотографии портретный снимок и картинка с новым окружением. Исследователи отмечают, что пока модель плохо справляется с альбедо, из-за чего некоторые типы одежды и глаза могут выглядеть неестественно.

Omnimatte

Доступность: страница проекта / статья

Работа со светом и тенью также нужна для качественного удаления объектов с изображений. Новая нейросеть от исследователей Google может автоматически связывать предметы в видео и вызванные ими эффекты в сцене. Это могут быть тени и отражения, а также рябь от объектов в воде или вообще посторонние объекты, движущиеся рядом, например, собака на поводке. На вход подается грубая маска объектов, а на выходе отдается два видео с фоном и с отдельно вырезанным объектом.

DeepFaceEditing

Доступность: страница проекта / репозиторий

Создатели объединили подход с преобразованием фото в карандашный набросок с возможностями управления скрытым пространством GAN для сохранения эффектов освещения, реалистичности текстур и т.д. Таким образом для редактирования на вход подается оригинальное фото лица человека, оно преобразуется в скетч, который можно изменять штрихами.

StyleMapGAN

Доступность: репозиторий

Новый нейросетевой фотошоп, на этот раз от исследователей из корейской компании Naver. Метод позволяет редактировать отдельные области изображений. Как и у решений, которые мы рассматривали в апреле, здесь та же задача управление скрытыми векторами генеративно-состязательной сети. В их подходе промежуточное скрытое пространство имеет пространственные измерения, и пространственно изменяющаяся модуляция заменяет адаптивную раздельную нормализацию. Таким образом кодировщик более точно создает вектора чем методы, основанные на оптимизации с сохранением свойств GAN.

GPEN

Доступность: онлайн-демо / статья / репозиторий

Китайские исследователи из Alibaba представили модель для реставрации размытых фото низкого качества, который в отличие от методов на основе GAN, создает не чрезмерно сглаженные изображения. Для этого модель использует GAN, чтобы сгенерировать высококачественное изображения лица, которое предварительно декодируется с помощью U-образной DNN.

CodeNet

Доступность: репозиторий

Исследователи из IBM представили крупнейший открытый датасет для проведения бенчмарков с участием программного кода. Набор данных содержит 500 миллионов строк кода на 55 языках программирования, включая C ++, Java, Python, Go, COBOL, Pascal и FORTRAN. CodeNet фокусируется на обнаружении сходств и отличий кода, чтобы продвигать разработку систем, которые смогут автоматически переводить код с одного языка программирования на другой.

DatasetGAN

Доступность: страница проекта / статья

Современные глубокие сети чрезвычайно требовательны к данным, поэтому обучение на крупномасштабных наборах данных требует много времени на разметку. NVIDIA представили генератор синтетических аннотированных датасетов для создания массивных наборов данных, который требует минимальных человеческих усилий. Метод основан на современных GAN и предлагает способ декодирования скрытого пространства для семантической сегментации изображений. Код обещают скоро опубликовать.

Golos

Доступность: репозиторий

Исследователи из Сбера опубликовали датасет с русским корпусом, подходящий для исследования речи. Набор данных в основном состоит из записанных и вручную размеченных аудиофайлов. Общая продолжительность аудиозаписи около 1240 часов.

В мае стали доступны:

На этом все, спасибо за внимание и до встречи в следующем месяце!

Подробнее..

Упадок RNN и LSTM сетей

04.06.2021 18:05:38 | Автор: admin

Автор: Eugenio Culurciello, оригинальное название: The fall of RNN / LSTM

Перевод: Давыдов А.Н.

Ссылка на оригинал

Мы полюбили RNN (рекуррентные нейронные сети), LSTM (Long-short term memory), и все их варианты. А теперь пора от них отказаться!

В 2014 году LSTM и RNN, были воскрешены. Но мы были молоды и неопытны. В течении нескольких лет они был способом решения таких задач как: последовательное обучение, перевод последовательностей (seq2seq). Так же они позволили добиться потрясающих результатов в понимании речи и переводе ее в текст. Эти сети поспособствовали восхождению таких голосовых помощников как Сири, Кортана, Гугл и Алекса. Не забудем и машинный перевод, который позволил нам переводить документы на разные языки. Или нейросетевой машинный перевод, позволяющий переводить изображения в текст, текст в изображения, делать субтитры для видео и т.д.

Затем, в последующие годы (2015-16) появились ResNet и Attention (Внимание). Тогда начало приходить понимание, что LSTM была умной техникой обойти, а не решить задачу. Так же Attention показал, что MLP сеть (Multi-Layer Perceptron Neural Networks -многослойные персептроны) может быть заменена усредняющими сетями, управляемыми вектором контекста. (более подробно об этом дальше).

Прошло всего 2 года, и сегодня мы можем однозначно сказать:

Завязывайте с RNN и LSTM, они не так хороши!

Можете не принимать наши слова на веру, просто посмотрите, что сети на основе Attention используют такие компании как Гугл, Фэйсбук, Сэйлфорс и это только некоторые из них. Все эти компании заменили RNN сети и их варианты на сети основанные на Attention и это только начало. Дни RNN сочтены во всех приложениях, так как они требуют больше ресурсов для обучения и работы, чем модели основанные на Attention.

Но почему?

Вспомним, что RNN, LSTM и их производные используют в основном последовательную обработку во времени. Обратите внимание на горизонтальную стрелку на диаграмме ниже:

Рис.1 Последовательность процессов в RNN сетяхРис.1 Последовательность процессов в RNN сетях

Она означает, что долгосрочная информация должна последовательно пройти через все ячейки, прежде чем попасть в текущую обрабатываемую ячейку. Это означает, что ее можно легко повредить, многократно умножая на малые числа близкие к 0, что является причиной исчезновения градиента.

На помощь пришел модуль LSTM. Который сегодня можно рассматривать, как многошлюзовый переключатель, немного похожий на ResNet. Он может обходить блоки(модули) и таким образом помнить более длительные временные отрезки. Таким образом у LSTM есть способ устранить некоторые проблемы с исчезающим градиентом. Но не все.

Рис.2 Последовательность процессов в LSTM Рис.2 Последовательность процессов в LSTM

У нас есть последовательный путь от старых ячеек к текущей. Фактически, путь теперь еще более сложен, так как оброс добавочными и забытыми ветвями. Несомненно, LSTM, GRU и их производные могут оперировать намного более долгосрочной информацией! Но они могут помнить последовательности из сотен (100), а не из тысяч или десятков тысяч.

И еще одна проблема RNN сетей заключается в том, что они очень требовательны к оборудованию. Требуют много ресурсов, как для обучения (это значит мы не можем обучить их быстро), так и для запуска. Для моделей основанных на RNN в облаке требуется много ресурсов. Учитывая, что потребность в преобразовании речи в текст быстро растет, а облако не масштабируется, нам потребуется производить обработку прямо на Amazon Echo!

Что нам делать?

На сентябрь 2018г я бы настойчиво рекомендовал бы рассмотреть этот подход - Постоянное внимание (Pervasive Attention)

Это 2D свёрточная нейронная сеть, которая может превзойти как модели RNN/LSTM, так и модели на основе Attention, такие как Transformer

Метод Transformer был отличным решением с 2017 года до статьи, ссылка на которую дана выше. Как уже говорилось ранее, он дает большие преимущества по ряду параметров.

В качестве альтернативы: если последовательной обработки следует избегать, мы можем найти блоки данных, которые смотрят вперед или, лучше сказать оглядываются назад, поскольку большую часть времени мы имеем дело с причинно-следственными данными в реальном времени (когда мы знаем прошлое и хотим повлиять на будущие решения) Другой случай, когда нам надо перевести предложение или проанализировать видео, у нас есть все данные и мы можем размышлять над ними больше времени. Такие блоки, смотрящие вперед/назад являются модулями нейронного внимания.

На помощь приходит иерархический нейронный кодировщик внимания, объединяющий несколько модулей нейронного внимания, показанный на рисунке ниже:

Рис.3 Иерархический нейронный кодировщик вниманиРис.3 Иерархический нейронный кодировщик внимани

Лучший способ заглянуть в прошлое - использовать модули внимания, чтобы суммировать все прошлые закодированные вектора в контекстный вектор Ct

Обратите внимание, что здесь есть иерархия модулей внимания, очень похожая на иерархию нейронных сетей. Это также похоже на временную свёрточную сеть (TCN), описанную в примечании 3 ниже.

В иерархическом нейронном кодировщике множество слоев Внимания могут смотреть на небольшую часть недавнего прошлого, скажем 100 векторов, в то время как слои выше могут смотреть на эти 100 модулей внимания смотрящие на эти 100 векторов. Эффективно интегрируя информацию 100х100 векторов. Это расширяет возможности иерархического кодировщика нейронного внимания до 10000 прошлых векторов.

Это способ заглянуть глубже в прошлое, чтобы эффективнее влиять на будущее.

Но что еще более важно, взгляните на длину пути, необходимого на распространение вектора данных, поданного на вход сети: в иерархических сетях, он пропорционален логарифму N (log(N)), где N количество уровней иерархии. Это контрастирует с шагами Т, которые должна выполнить RNN, где Т максимальная длина запоминаемой последовательности. Как мы видимо T многократно больше N (T>>N)

Последовательности легче запомнить, если данные проходят через 3-4 слоя нежели чем через 100!

Эта архитектура похожа на нейронную машину Тьюринга, но позволяет нейронной сети решать, какую информацию считывать из памяти посредством внимания. Это означает, что реальная нейронная сеть будет решать, какие вектора из прошлого важны для будущих решений.

Но как на счет объема памяти? Вышеупомянутая архитектура сохраняет все предыдущие данные в памяти, в отличии от нейронной машины Тьюринга. Это, кажется, не очень эффективным: представьте, что мы будем хранить данные о каждом кадре в видео в большинстве случаев вектор данных не меняется от кадра к кадру, поэтому мы будем хранить слишком много данных об одном и том же. Что мы можем сделать, так это добавить еще один модуль, чтобы предотвратить сохранение коррелированных данных. Например, не запоминать вектора слишком похожие на ранее запомненные. Но это реально геморрой, лучше всего позволить нейросети самой решить, какие вектора запоминать, а какие нет.

В итоге забудьте о RNN и вариантах. Используйте Attention. Внимание, - действительно всё, что вам нужно!

Дополнительная информация

О тренировке RNN\LSTM: RNN и LSTM сложно обучить, потому что они требуют вычислений с ограничением полосы пропускания памяти, что является худшим кошмаром для разработчика оборудования и в конечном итоге ограничивает применимость решений на основе таких нейронных сетей. Короче говоря, LSTM требует 4 линейных слоя (слой MLP) на ячейку для работы на каждом временном шаге последовательности. Для вычисления линейных слоев требуется большая пропускная способность памяти, фактически они не могут часто использовать много вычислительных единиц, потому что системе не хватает пропускной способности памяти для питания вычислительных единиц. И легко добавить больше вычислительных блоков, но сложно добавить больше пропускной способности памяти (обратите внимание на достаточное количество строк на микросхеме, длинные провода от процессора к памяти и т. Д.). В результате RNN / LSTM и их варианты не подходят для аппаратного ускорения, и мы говорили об этой проблеме раньше здесь и здесь. Решение будет вычисляться в устройствах памяти, подобных тем, над которыми мы работаем в FWDNXT.

Примечание

1: Иерархическое нейронное внимание похоже на идеи в WaveNet. Но вместо свёрточной нейронной сети мы используем иерархические модули внимания. Также: иерархическое нейронное внимание может быть двунаправленным.

2: RNN и LSTM - это проблемы с ограниченной пропускной способностью памяти (подробности см. Здесь). Блоку (ам) обработки требуется столько пропускной способности памяти, сколько операций / с они могут обеспечить, что делает невозможным их полное использование! Внешней пропускной способности никогда не будет достаточно, и способ немного решить проблему - использовать внутренние быстрые кеши с высокой пропускной способностью. Наилучший способ - использовать методы, которые не требуют перемещения большого количества параметров из памяти взад и вперед или которые могут быть повторно использованы для многократных вычислений на каждый передаваемый байт (высокая арифметическая интенсивность).

3: вот статья, в которой CNN сравнивается с RNN. Временная сверточная сеть (TCN) превосходит канонические рекуррентные сети, такие как LSTM, в разнообразном диапазоне задач и наборов данных, демонстрируя при этом более эффективную память.

4: С этой темой связан тот факт, что мы мало знаем о том, как наш человеческий мозг учится и запоминает последовательности. Мы часто изучаем и запоминаем длинные последовательности в более мелких сегментах, например, телефонный номер 858 534 22 30, запоминаемый в виде четырех сегментов. Поведенческие эксперименты предполагают, что люди и некоторые животные используют эту стратегию разбиения когнитивных или поведенческих последовательностей на фрагменты для решения широкого круга задач - эти фрагменты напоминают мне небольшие свёрточные сети или сети, подобные Вниманию (Attention), на более мелких последовательностях, которые затем иерархически связаны друг с другом, как в иерархическом кодировщике нейронного внимания и временной сверточной сети (TCN). Дополнительные исследования заставляют меня думать, что рабочая память похожа на сети RNN, которые используют рекуррентные реальные нейронные сети, и их емкость очень мала. С другой стороны, кора и гиппокамп дают нам возможность запоминать действительно длинные последовательности шагов (например, где я припарковал свою машину в аэропорту 5 дней назад), предполагая, что может быть задействовано больше параллельных путей для запоминания длинных последовательностей, где механизм внимания блокирует важные фрагменты и форсирует прыжки в частях последовательности, которые не имеют отношения к конечной цели или задаче.

5: Приведенные выше свидетельства показывают, что мы не читаем последовательно, фактически мы интерпретируем символы, слова и предложения как группу. Основанный на внимании или свёрточный модуль воспринимает последовательность и проецирует представление в нашем сознании. Мы не ошиблись бы в этом, если бы обрабатывали эту информацию последовательно! Остановимся и заметим нестыковки!

6: Недавняя статья показывающая обучение без использования методов Attention или Transformer, показала удивительную эффективность в обучении без учителя. VGG или NLP? Эта работа также является продолжением новаторской работы Джереми и Себастьяна, где LSTM со специальными процедурами обучения смог научиться без учителя предсказывать следующее слово в последовательности текста, а затем также мог передавать эти знания новым задачам. Здесь приводится сравнение эффективности LSTM и Transformer (на основе внимания), которое показывает, что внимание обычно побеждает, и что обычный LSTM превосходит Transformer на наборе данных - MRPC

7: Здесь вы можете найти отличное объяснение архитектуры Transformer и потока данных!

Подробнее..
Категории: Python , Lstm , Tensorflow , Attention , Rnn

Виртуальные машины А2 крупнейшие облачные образы с графическими процессорами NVIDIA A100 теперь доступны для всех

20.04.2021 12:16:22 | Автор: admin

Недавно, в нашем Google Cloud блоге, мы анонсировали, что в сервисе Compute Engine появились виртуальные машины A2 на базе графических процессоров NVIDIA Ampere A100 с тензорными ядрами. С их помощью пользователи смогут выполнятьмашинное обучениеивысокопроизводительные вычисленияна базе архитектуры NVIDIA CUDA, увеличивая рабочие нагрузки за меньшее время и цену.

В этой статье, мы хотим рассказать подробнее о том, что представляют из себя виртуальные машины А2, об их производительности и особенностях. И рассказать о том, как мы используют эти машины наши коллеги и партнеры.

Высочайшая производительность

Одна ВМ A2 поддерживает до 16графических процессоров NVIDIA A100. На сегодняшний день это самый производительный экземпляр графического процессора на одном узле среди всех конкурирующих решений от крупнейших поставщиков облачных услуг. В зависимости от масштабов рабочей нагрузкивы также можете выбрать виртуальные машины A2 с меньшим числом графических процессоров (1, 2, 4 и 8).

Конфигурации ВМ A2 доступные в сервисе Compute EngineКонфигурации ВМ A2 доступные в сервисе Compute Engine

Это позволяет исследователям, специалистам по обработке данных и разработчикам значительно увеличивать производительность масштабируемых рабочих нагрузок (например, машинное обучение, логический вывод и высокопроизводительные вычисления) на архитектуре CUDA. Семейство ВМ A2 на платформе Google Cloud Platform способно удовлетворить потребности самых требовательных приложений для высокопроизводительных вычислений, например при моделировании методами вычислительной гидродинамики вAltair ultraFluidX.

Для тех, кому нужны сверхпроизводительные системы, Google Cloud предлагает кластеры из тысяч графических процессоров для распределенного машинного обучения, а также оптимизированные библиотеки NCCL для горизонтального масштабирования. Версия ВМ с 16 графическими процессорами A100, объединенными через шинуNVIDIA NVLink, это уникальное предложение Google Cloud. Если вам нужно масштабировать требовательные рабочие нагрузки по вертикали, можно начать с одного графического процессора A100 и довести их число до 16 без настройки нескольких ВМ для машинного обучения на одном узле.

Новая ВМ A2-MegaGPU: 16 графических процессоров A100 со скоростью передачи данных 9,6 ТБ/с по интерфейсу NVIDIA NVLinkНовая ВМ A2-MegaGPU: 16 графических процессоров A100 со скоростью передачи данных 9,6 ТБ/с по интерфейсу NVIDIA NVLink

Чтобы удовлетворить потребности разных приложений, доступны и менее производительные конфигурации ВМ A2 с встроенным SSD-диском на 3ТБ, который ускоряет доставку данных в графический процессор. Так, графический процессор A100 в Google Cloud более чем в 10раз увеличивает скорость предварительного обучения модели BERT-Large по сравнению с NVIDIA V100 прошлого поколения. При этом в конфигурациях с числом графических процессоров от 8 до 16 наблюдается линейный рост производительности. Кроме того, разработчики могут использовать предварительно настроенное ПО в контейнерах из хранилища NVIDIANGCдля быстрого запуска экземпляров A100 в Compute Engine.

Отзывы пользователей

Мы стали предлагать ВМ A2 с графическими процессорами A100 нашим партнерам в июле 2020 года. Сегодня мы работаем со множеством организаций и помогаем им достигать новых высот в области машинного обучения, визуализации и высокопроизводительных вычислений. Вот что они говорят о виртуальных машинах А2:

КомпаниюDessaнедавно приобрел холдинг Square. Она занимается исследованиями в сфере ИИ и стала использовать ВМ A2 одной из первых. На базе ее экспериментов и инноваций Square разрабатывает персонализированные сервисы и умные инструменты для Cash App, которые с помощью ИИ помогают неспециалистампринимать более взвешенные финансовые решения.

"Благодаря Google Cloud мы получили необходимый контроль над своими процессами, говорит Кайл де Фрейтас, старший разработчик ПО в Dessa. Мы понимали, что предлагаемые в Compute Engine ВМ A2 на базе графических процессоровNVIDIA A100с тензорными ядрами способны радикально сократить время вычислений и значительно ускорить наши эксперименты. Процессоры NVIDIA A100, используемые в Google Cloud AI Platform, позволяют нам эффективно развивать инновации и воплощать в жизнь новые идеи для наших клиентов".

Hyperconnect это международная компания, занимающаяся видеотехнологиями в сфере коммуникаций (WebRTC) и ИИ. Hyperconnect стремится объединять людей во всем мире и для этого создает сервисы на базе различных технологий обработки видео и ИИ.

"Экземпляры A2 с новыми графическими процессорами NVIDIA A100 на платформе Google Cloud поднимают производительность на совершенно новый уровень при настройке моделей глубокого обучения. Мы легко перешли на них с прошлого поколения графических процессоров V100. Благодаря конфигурации ВМ A2-MegaGPU мы не только ускорили обучение более чем в два раза по сравнению с V100, но и получили возможность масштабировать по вертикали рабочие нагрузки с большими нейронными сетями в Google Cloud. Эти инновации помогут нам оптимизировать модели и повышать удобство использования сервисов Hyperconnect", говорит Ким Бемсу, исследователь по машинному обучению в Hyperconnect.

DeepMind(дочерняя компания Alphabet) это команда ученых, инженеров, специалистов по машинному обучению и других экспертов, которые развивают технологии ИИ.

"DeepMind занимается искусственным интеллектом. Наши исследователи проводят различные эксперименты в этой сфере с применением аппаратных ускорителей. Благодаря Google Cloud мы получили доступ к новому поколению графических процессоров NVIDIA, а виртуальная машина A2-MegaGPU-16G позволяет проводить обучение моделей быстрее, чем когда-либо. Мы с радостью продолжаем работать с платформой Google Cloud, которая поможет нам создавать будущую инфраструктуру машинного обучения и ИИ", Корай Кавукчуоглу (Koray Kavukcuoglu), вице-президент DeepMind по исследовательской деятельности.

AI2 это некоммерческий исследовательский институт, занимающийся перспективными исследованиями и разработками в сфере ИИ для общего блага.

"Наша основная миссия расширение возможностей компьютеров. В связи с этим мы сталкиваемся с двумя фундаментальными проблемами. Во-первых, современные алгоритмы ИИ требуют огромных вычислительных мощностей. Во-вторых, специализированное оборудование и ПО в этой области быстро меняются. И с этим нужно что-то делать. Процессоры A100 в GCP в четыре раза производительнее наших нынешних систем, и для их использования не требуется серьезно перерабатывать программный код. По большому счету достаточно минимальных изменений. Графический процессор A100 в Google Cloud позволяет значительно увеличить количество вычислений на доллар. Соответственно, мы можем проводить больше экспериментов и использовать больше данных", говорит Дирк Груневельд, старший разработчик Allen Institute for Artificial Intelligence.

OTOY это компания, которая занимается облачными графическими вычислениями. Она развивает инновационные технологии создания и доставки контента для средств массовой информации и индустрии развлечений.

"Уже около десяти лет мы расширяем границы возможного в сфере графической визуализации и облачных вычислений и стремимся устранить ограничения для художественного творчества. Благодаря процессорам NVIDIA A100 в Google Cloud с большим объемом видеопамяти и самым высоким рейтингом OctaneBench за всю историю мы первыми достигли уровня, когда художникам при реализации своих замыслов больше не нужно задумываться о сложности прорисовки. Система визуализации OctaneRender снизила стоимость спецэффектов. Она позволяет любому разработчику с графическим процессором NVIDIA создавать великолепную картинку кинематографического качества. Виртуальные машины с процессорами NVIDIA A100 в Google Cloud предоставляют пользователям OctaneRender и RNDR доступ к современным графическим процессорам NVIDIA, прежде доступным только для крупнейших голливудских студий", говорит Джулз Урбах, основатель и генеральный директор OTOY.

Цены и доступность графических процессоров

Экземпляры NVIDIA A100 теперь доступны в следующих регионах: us-central1, asia-southeast1 и europe-west4. В течение 2021года к ним добавятся дополнительные регионы. ВМ A2 в Compute Engine доступны по запросу со скидкой за вытесняемые экземпляры и обязательство по использованию, а также полностью поддерживаются в Google Kubernetes Engine (GKE), Cloud AI Platform и других сервисах Google Cloud. A100 предлагаются по цене всего 0,87доллара США за один графический процессор в вытесняемых ВМ A2. С полным прейскурантом можно ознакомитьсяздесь.

Начало работы

Вы можете быстро развернуть работу, приступить к обучению моделей и выполнять рабочие нагрузки с логическим выводом на графических процессорах NVIDIA A100 с помощьюобразов ВМ для глубокого обученияв доступных регионах. В этих образах собрано все необходимое ПО: драйверы, библиотеки NVIDIA CUDA-X AI и популярные фреймворки для ИИ, такие как TensorFlow и PyTorch. Оптимизированныеобразы TensorFlow Enterpriseтакже включают поддержку A100 для текущих и прошлых версий TensorFlow (1.15, 2.1 и 2.3). Вам не нужно беспокоиться об обновлении ПО, совместимости и настройке производительности всё это мы берем на себя. Наэтой страницеприводятся сведения о доступных в Google Cloud графических процессорах.


Напоминаем что при первой регистрации в Google Cloud: вам доступны бонусы на сумму 300 долларов США, а более 20 бесплатных продуктов доступны всегда. Подробнее поспециальной ссылке.

А так же выражаем благодарность за помощь в подготовке материала коллегам: Бхарат Партасарати, Крис Клебан и Звиад Кардава

Подробнее..

Перевод Как магия машинного обученияменяет нашу жизнь

05.04.2021 16:10:13 | Автор: admin

Много лет назад я загорелась идеей научиться программированию, создав собственный сайт. Тогда я ничего не понимала в компьютерах и тем более в серверах. И только одна мысль о том, сколько же мне предстоит узнать нового, будила во мне необыкновенный интерес. Перед сном я обдумывала сотни вариантов своего сайта от параллакс-эффекта при прокручивании до шрифтов из Google Fonts и мечтала о будущих проектах.

Прошли годы, и теперь я профессиональный инженер и решаю серьезные технологические задачи они действительно гораздо сложнее, чем мой первый сайт! Тем не менее, я часто вспоминаю ощущения и эмоции, которые испытала тогда, делая первые шаги в программировании.

Один из веселых способов познакомиться с машинным обучением это создать что-то для себя. В этой статье я расскажу, как это сделать.

Работая в сфере технологий, вы посвящаете свою жизнь учебе. Глазом не успеешь моргнуть, как самое совершенное ПО моментально заменяется чем-то более продвинутым (хотя я все никак не могу отвыкнуть от старого доброго Vim).

Одно из интереснейших направлений в ИТ машинное обучение. Большинству из нас не рассказывали о нем в вузах (а у некоторых вообще не было уроков информатики), но скоро машинное обучение станет повсеместным, и оно изменит процесс разработки ПО во всех областях. Неудивительно, что меня часто спрашивают, с чего лучше начать изучение машинного обучения.

Обычно я советую обратиться к таким ресурсам, как курс компании Google под названиемMachine Learning Crash Course, а также книгаПрикладное машинное обучение с помощью Scikit-Learn, Keras и TensorFlowи курс на сайте CourseraМашинное обучение(автор: Andrew Ng), который нацелен не только на теоретические основы, но и на практику.

Но если вы, как и я, предпочитаете сразу переходить к делу, попробуйте познакомиться с машинным обучением, создав для себя программу. Собственные проекты это не только приятный и полезный способ знакомства с новыми технологиями. В отличие от идеальных условий, которые предлагаются в домашних заданиях, на практике вы столкнетесь с реальными трудностями при внедрении машинного обучения в ПО.

В начале пандемии я вдруг поняла, что у меня много свободного времени. И я поставила перед собой задачу узнать больше о машинном обучении, решая с его помощью повседневные задачи. С тех пор машинное обучение помогает мне искать семейные видео, улучшать подачу во время игры в теннис, переводить видео, создавать новые рецепты выпечки и многое другое.

Ниже вы найдете список и обзор всех этих проектов, а такжеисходный код, обучающиевидео на YouTubeи пошаговые инструкции вмоем блоге. Я расскажу обо всем процессе от работы с новыми технологиями и инструментами до создания приложения с их помощью. Надеюсь, эти проекты окажутся для вас не только веселыми, но и полезными. А если они вдохновят вас на собственные проекты с машинным обучением, я буду только рада. Не забудьтерассказать мнео своих свершениях в твиттере. Удачной работы!

Внедрение машинного обучения в свои проекты

Умный архив семейных видео

  • Вы создадите: архив, который сможет предоставлять видео по фразе или объекту из записи (например, "день рождения", "велосипед" или "видеоигры").

  • Вы узнаете:

    • как применять машинное обучение в сортировке и поиске сложных типов данных;

    • как использовать Video Intelligence API;

    • как проектировать приложение, в основе которого лежит машинное обучение (в этом помогут инструментыFlutter для создания клиентской части,Firebase для написания кода без использования серверов, и поиск как сервис, предоставленныйAlgolia).

Бот-модератор в Discord

  • Вы создадите: бот для чат-платформыDiscord,который помогает находить оскорбительные и нецензурные сообщения, а также спам.

  • Вы узнаете:

    • как использоватьPerspective APIдля анализа текста;

    • как применять машинное обучение в приложениях для чата;

    • как выбирать, нужно ли машинное обучение в сложных и неоднозначных ситуациях.

  • Вы создадите: блокнот Jupyter, который отслеживает подачу и траекторию теннисного мяча (может также пригодиться в гольфе и баскетболе), а также анализирует данные, чтобы дать полезные советы. Для этого перейдите вQwiklabs.

  • Вы узнаете:

    • как выполнять сложное машинное обучение с помощью небольших наборов данных;

    • как комбинировать простые математические вычисления с распознаванием поз для понимания движений человека;

    • как использовать Video Intelligence API;

    • как работать c AutoML Vision.

Умный игровой мир с технологией обработки естественного языка

илиСоздание приложений на основе языка с помощью семантического машинного обучения

  • Вы создадите:

    • простую систему на основе языка, с помощью которой можно взаимодействовать с игровым миром через ввод текста.

  • Вы узнаете:

    • как использовать одну из самых полезных методик обработки естественного языка встраивание предложений;

    • как реализовывать семантический поиск текста;

    • как разделять текст на кластеры;

    • как добавлять простые чат-боты;

    • как выполнять эти действия в Google Таблице.

Преобразование PDF-документа в аудиокнигу

  • Вы создадите: код, который преобразует PDF-файлы в аудиокниги формата MP3.

  • Вы узнаете:

    • как извлекать текст из PDF-файлов при помощи Vision API;

    • как озвучивать текст при помощи Text-to-Speech API;

    • как использовать математические вычисления для разделения макетов документа.

Перевод и озвучивание видео с помощью машинного обучения

  • Вы создадите: код, который автоматически преобразовывает речь из видео в текст, а затем переводит и озвучивает его.

  • Вы узнаете:

    • как совмещать технологии распознавания, перевода и синтеза речи;

    • как улучшать качество перевода и преобразования речи в текст;

    • как работать с видео и аудио на языке Python.

Создание рецептов выпечки с помощью ИИ

  • Вы создадите: модель машинного обучения без единой строки кода, которая может классифицировать рецепты и генерировать новые.

  • Вы узнаете:

    • как создавать модели машинного обучения в AutoML Tables с помощью табличных данных без написания кода;

    • как определять причину решений модели с помощью функций.

Создание модели машинного обучения в браузере без написания кода

  • Вы создадите: быструю модель машинного обучения, которая распознает позы, объекты и звуки.

  • Вы узнаете:

    • что нужно, чтобы создать простую модель машинного обучения без написания кода;

    • как с помощью инструмента "Обучаемая машина" создать быструю модель, которую можно запустить в браузере.

Создание образов с помощью ИИ

  • Вы создадите: приложение, которое будет рекомендовать образы на основе фотографий вашего гардероба и публикаций медийных персон в соцсетях.

  • Вы узнаете:

    • как использовать Product Search и Vision API;

    • как проектировать приложения на основе машинного обучения с помощью React и Firebase.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru