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

Глубокое обучение

Перевод Обнаружение эмоций на лице в браузере с помощью глубокого обучения и 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% скидки на обучение.

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

Собрать сервер для глубокого обучения за пол ляма может и ребенок. Или нет?

25.03.2021 22:22:00 | Автор: admin

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

Сетап

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

Зачем вообще оно нужно? Если вы все знаете, то переходите сразу к фазе описания выбора мной компонентов. Или читайте дальше! Сервер такой же компьютер, как тот, что стоит у вас на столе, но рассчитанный на долгую нагрузку и собирают его обычно из других деталей. Разница примерно как с автомобилем массового автопрома и спецтехникой вроде грузовика. Он может не быть быстрее, но должен выдерживать большую нагрузку (количество пользователей) и дистанции (время работы под нагрузкой для серверов это могут быть годы). Зачем оно нам? Мы создаем высокополигональные (~1 млн) 3D модели для игр и кино на основе фото, и сейчас занимаемся разработкой инновационных алгоритмов на основе машинного обучения для этой задачи.

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

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

В общем, обычно в компьютере может не быть выделенной видеокарты, но тут их должно было быть несколько. Причем именно RTX 3090!? Это не такая простая задача, как кажется.

Изучив вопрос, я пришел к выводу, что невпихуемые восемь прожорливых видеокарт можно впихнуть только на серверной платформе (http://personeltest.ru/aways/www.gigabyte.com/Enterprise/GPU-Server) для GPU. Но даже если такие вообще можно будет найти в России, то стоить это будет ровно полмиллиона, просто за корпус и материнку (без карт и процессоров). Тогда я пораскинул мозгами и предложил три варианта, каждый содержал решение своей задачи.

Первая опция

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

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

AMD Ryzen 7 Vermeer 5800X

Asus PRIME X570-P

Прежде чем я опишу более сложные варианты, нужно сказать об особенностях процессора. Для подключения видеокарты мы используем линию PCI Express. По сути это такой же интерфейс как USB, с которым все знакомы, но только высокоскоростной и внутри самого компьютера. Причем устроен он весьма забавно. Представьте себе автотрассу. У нее есть полосы и ограничение скорости. Вот линии PCI это один в один, как трасса, где количество машин, проезжающих в секунду, определяет скорость передачи информации. Если мы возьмем трассу в четыре полосы, то машин проедет в два раза быстрее, но то же самое произойдет, если мы сделаем каждую полосу ровно в два раза быстрее. За количество полос у PCI отвечает так называемое количество шин (проводников), а за скорость поколение PCI.

Таким образом фразу: PCI-E 3.0 4x написанную на устройстве нужно читать как данное устройство займет четыре полосы трассы с максимальной скоростью 3. Видеокарты могут занимать до 16 линий PCI, причем это число может быть и меньше. То есть чисто технически видеокарта может работать и от одной линии. Именно так поступают майнеры, когда подключают 16 видеокарт к одному слоту. Они просто разбивают огромную трассу на 16 полос, жертвуя скоростью, зато не приходится покупать 16 компов. Для их приложений скорость не так нужна. В целом, правило пальца такое. Допустим, если карта подключена в 16 линий то это 100% производительности, тогда как показывает практика, например, для игр, при использовании восьми линий, она теряет 5% производительности, а при использовании четырех уже около 20-30% или больше. Для разных приложений эти цифры немного отличаются. У предложенного процессора AMD Ryzen 7 Vermeer 5800X всего 24 линий PCI, что является стандартным числом для даже очень дорогих процессоров для настольных ПК. 24 линий более чем достаточно для подключения одной-двух видеокарт и еще какой-то периферии вроде звуковой карты и NVME накопителя. Сложно представить, чтобы их не хватило. Но вот воткнуть в него 4 видеокарты без особых потерь уже не получится. Машины просто начнут стоять в пробках. Тут на ум приходит вторая опция.

Вторая опция

Собрать компьютер вокруг серверного процессора. Теперь уже это кажется оправданным. У него количество линий PCI может измеряться не десятками, а сотнями (обычные смертные этим не пользуются, а вот серверное железо да). Таким образом, если найти подходящую материнскую плату, то можно будет гарантированно разместить туда много видеокарт. Был выбран пограничный вариант: AMD Ryzen Threadripper 2 2920X c аж 64 линиями PCI 3.0. Причем он так и позиционируется производителем как серверный процессор, но адаптированный для простых нормизов, которым нужна какая-то специфика промышленного железа. Например, для высокопроизводительных станций для видеомонтажа, где должно работать несколько человек и т. д. в материнскую плату, подобранную для него (ASRock X399 Taichi), влезало 4 видеокарты без адаптеров. Что уже лучше, чем обычный игровой комп, при стоимости такой сборки всего на 50 тысяч дороже обычной игровой (150 вместо примерно 100). Но и процессор тут уже совсем другой ценовой категории, пусть и довольно дешевый среди своих напарников по цеху. При цене в 60-70 тысяч этот монстр выдает аж 24 потока, что кажется и немного для его цены, но если добавить поддержку ECC памяти, много шин PCI, большой кэш, получается приятно, если учитывать то, ради чего мы его берем.

Третий и последний вариант

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

Выбор

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

Итоговая сборка

Ниже приведу итоговую сборку как мы ее заказали:

CPU

AMD Ryzen Threadripper 2 2920X BOX

https://www.e-katalog.ru/AMD-2920X-BOX.htm

1

85

MB

ASRock X399 Taichi

https://www.e-katalog.ru/ASROCK-X399-TAICHI.htm

1

25

PSU1

Fractal Design Ion+ Platinum FD-PSU-IONP-860P-BK

https://www.e-katalog.ru/FRACTAL-DESIGN-FD-PSU-IONP-860P-BK.htm

1

14

CASE

Fractal Design MESHIFY S2

https://www.e-katalog.ru/FRACTAL-DESIGN-MESHIFY-S2.htm

1

12

SSD

Samsung 860 EVO MZ-76E2T0BW 2 ТБ

https://www.e-katalog.ru/SAMSUNG-MZ-76E2T0BW.htm

1

17

CPU cooler

Be quiet Dark Rock Pro TR4

https://www.e-katalog.ru/BE-QUIET-DARK-ROCK-PRO-TR4.htm

1

7

Coolers

140

https://www.e-katalog.ru/FRACTAL-DESIGN-DYNAMIC-X2-GP-14.htm

2

3

120

https://www.e-katalog.ru/ID-COOLING-PL-12025-W.htm

2

1

SUM:

164

Итого: 164 тысячи рублей. Вроде неплохо, учитывая что цена на RTX 3090 на этот момент стоили уже 220 тысяч, и я убедил, что, возможно, 4х видеокарт может и хватить. Теперь по компонентам отдельно, как я думал о них до сборки:

Процессор

2920X обычно не востребованный из-за разницы со своими старшими братьями постепенно вытеснялся 3м поколением тредрипперов как раз упал в цене, это был хороший выбор (как показалось). Отдельная тема это установка процессора "Threadripper". Самые важные моменты: отвертка, которая идет в комплекте не обычная, а заряженная пружиной, чтобы контролировать натяжение, поэтому вскрывать сокет и устанавливать процессор нужно ТОЛЬКО ей. И только в порядке, предписанном на крышке розетки процессора (сокета). На рисунке видно порядок установки.

Материнская плата

ASRock X399 Taichi, средний выбор для такого железа обладала всеми необходимыми приятностями: 8 слотов для памяти, зачем-то встроенный wifi...

Но с материнской платой вышло больше всего проблем. Представьте ваше лицо, когда вы на стенде собираете компоненты стоимостью 150К включаете их, а они не дают признаки жизни Но я не растерялся, понял что блок питания не подает питание на материнку. На плате работало служебное 3В питание, была исправна батарейка. Сброс CMOS не помог. Коротких замыканий ни на какой линии питания не было. Меня сбил сначала тот факт, что от служебного питания на ней запитывалась подсветка. Начал грешить на блок питания, но нет. Проверив его по методике ниже, оказалось, что материнская плата все же не подает сигнал на исправный блок питания. Моя интуиция подсказала, что скорее всего это неправильное поведение. В гарантийном отделе KNS меня стали уверять что дело в неправильной версии BIOS материнской платы, и я, не заметив на самой плате наклейку, утверждающую, что BIOS последний, поехал искать где его обновить. Возле гарантийного центра меня встретили только очень пугающие ребята. Один немолодой человек, увидев у меня материнскую плату с символикой AMD, начал буквально кричать на весь ТЦ: AMD для нас не компьютер, а другие предложили обновить его за 3000р., но при условии что у меня будет подходящий процессор. Как будто был бы у меня процессор, я бы не смог обновить его сам, при условии, что для таких плат для этого просто нужно вставить флешку с кодом. Кто не знает, код BIOS (базовая система ввода вывода) отвечает за процесс запуска и первичную настройку и тест процессора, еще до старта любой операционной системы. Проблема, что если версия биоса старая, то компьютер просто не понимает, что в него вообще вставлен процессор. Тогда самый простой вариант вставить процессор более старой серии и обновить BIOS. Проблема в том, что процессоров Ryzen Threadripper первого поколения в москве в сервисных центрах почти не найти, что добавляло сложность моим изысканиям. Была ли это попытка, чтобы я пролетел с двухнедельным сроком возврата бракованного товара или нет, я не знаю. В определенном сервисе на Савеловской мне совершенно бесплатно подтвердили, что BIOS на плате самый свежайший, и там повторно оно не завелось уже на их стенде, но с моим процессором. И вот в самый последний день я отвез это в KNS и, уже уверив их, что их гипотеза не верна (и было бы странно, ибо плата вообще не стартовала блок питания), я отдал плату на гарантию. Через две недели они дождались свой процессор, и оказалось, что моя теория верна и плата мертва. Еще через день мы получили новую и продолжили сборку!

Охлаждение процессора

Охлаждать процессор, выделяющий тепла почти как четверть бытового обогревателя, предложено было кулером Be quiet Dark Rock Pro TR4, специально созданного для такого горячего процессора. Из особенностей скажу, что обычно элитная фирма Be quiet!, в этот раз немного разочаровала: установка кулера очень не эргономична для того, чтобы его закрепить или снять, нужно сначала вынуть центральный кулер (у него их три), потом особой комплектной длинной отверткой через особые отверстия отвинтить болты, только после этого отпустит клемму, которая и держит процессор. Вы можете посмотреть про этот кулер тут.

Блок питания

Выбор блока питания. Самая мистифицированная деталь компьютера, а так же самая частая ошибка: экономия на блоке питания. Причем, как правило, людям либо кажется, что больше мощности равно лучше, кому-то кажется, что много мощности плохо предлагаю разобраться. Блок питания берет переменный ток из розетки и преобразовывает его в набор постоянных напряжений (3.3,5,12,-12 Вольт). Все эти стандарты питания важны для разных компонентов, но самая важная линия это 12 Вольт. Именно от нее будут питаться все самые прожорливые компоненты. Именно от 12В питается процессор и видеокарта. Что же такое амперы на блоке питания? Ну, вы можете думать, что вольты это просто тип питания, примерно, как октановое число бензина. Вы приезжаете на бензоколонку и ожидаете увидеть 92,95 бензин. Точно так же работает и блок питания. Он предоставляет разное топливо. Причем напряжение, как и бензин, может быть плохим. Например, если под нагрузкой 12 Вольт превратились в 11, (а карета в тыкву), то это сродни тому, как если бы в тяжелые дни на заправке из-за нехватки 95го бензина его начинают бадяжить водой. А вот ток или мощность можно сравнить с литрами в минуту, которые заправка может выдавать. То есть, если на зарядке вашего телефона написано 5В 2А, это значит, что она может выдать не больше 2А по линии 5В. При этом при приближении к этим 2А качество напряжения может начать портиться, а зарядка греться и потеть. Именно поэтому все так любят брать блоки питания пожирнее. Например, кто-то скажет что и 1000 Ватт мало для RTX3090, что очевидно неверно, ибо сама по себе RTX 3090 потребляет по заявлению производителя 350 Ватт. Откуда же требование к блоку питания в более чем 750 Ватт? Давайте посчитаем! Дабы узнать сколько ест компонент, достаточно посмотреть на его тепловыделение, оно же энергопотребление. Грубо говоря, каждый компонент потребляющий ток, похож на ту же лампочку накаливания: пропустить ток греется. Например, если написано, что TDP процессора 60Ватт, значит, он будет выделять это тепло потребляя амперы по 12В линии. Чтобы получить ватты, нужно умножить ток на напряжение (IU=P). Или же, чтобы найти ток, нужно поделить 60 на 12. То есть 60-ти ваттный процессор потребляет 5А по 12В линии. Наш процессор потребляет целых 250 Ватт и видеокарта 350. Итого: по 12ти вольтовой линии блок питания должен выдать аж 600 Ватт.

Требование на 750 появляется из двух соображений, во-первых, многие производители льстят себе и пишут значения, при которых их продукции становится уже очень плохо, а во-вторых, из-за потерь в тепло везде, кроме потребителей, сколько-то съедят вентиляторы (по 2 Ватта каждый), сколько-то диски. В общем, мощности в 860 Ватт при условии выбора хорошего блока питания должно было хватить с головой. Я взял Fractal Design Ion+ Platinum FD-PSU-IONP-860P-BK. Не самый дорогой, но и не дешевый модульный блок питания от известного бренда. Характеристики его максимальных токов указаны на обратной части. Вы спросите, почему же ты не взял сразу блок питания с запасом на 4 видеокарты? Ну, когда я посмотрел цены на качественные блоки питания от 1000 Ватт, оказалось, что цена на них соизмерима с ценой всего компьютера. Сисоник на 1000 Ватт стоил аж 80К рублей. Но я, будучи электронщиком, понимал, что мне ничего не мешает вставить туда еще один блок питания специально для остальных видеокарт. Можно даже использовать компактный серверный блок, важно только сделать систему, которая бы включала блок питания одновременно с первым, но это несложно. Блоки питания включаются, как только напряжение на контакте PS_ON (см. рисунок) падает до нуля. То есть если вам хочется самим проверить блок питания без материнской платы, достаточно булавкой или скрепкой замкнуть контакты PS_ON и COM, и на остальных линиях появятся напряжения (Хоть все блоки питания и оборудованы защитами, но соблюдайте осторожность при работе с питанием, не допускайте попадание металлических компонентов на контакты, не вскрывайте блок питания). До этого момента включения не работает. Именно эти контакты замыкает материнская плата. То есть нужно было просто спаять плату, чтобы замыкать один контакт с другим, и можно сэкономить более 50ти тысяч рублей и подключать сколько хочешь мощных видеокарт. Теперь переходим к корпусу, который позволил все это безумие.


Корпус

Fractal Design MESHIFY S2, один из самых удобных корпусов, что я видел. Огромный, все быстро снимается. Внутри есть разветвитель PWM, чтобы можно было натыкать десятки вентиляторов, при этом заняв один слот на материнской плате. Оптимистично в него можно вставить до 6ти карт. Реалистично около четырех полноразмерных турбовинтовых карт. И то, если убрать нижнюю корзину для дисков, и разместить одну карту боком. Но иначе есть смысл брать только серверный корпус с переходником PCI, но такие в России найти вообще в продаже мне не удалось, только если заказывать на сайте DELL в США. Поэтому по факту взяли самый удобный корпус для большой рабочей или игровой системы. Из минусов могу выделить только встроенные очень слабые вентиляторы, которых тут установлено аж 3. Для высокопроизводительной системы советую вынуть их и заменить на высоко оборотистые управляемые 4 pin кулеры. У стоковых фиксированная скорость в 1000 оборотов, что хорошо для тихого ПК, но не очень для корпуса, которому предстоит рассеивать 800 Ватт тепла.

Память

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

Цели

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

Тесты

Про производительность отдельных компонентов системы вы можете найти много информации в Интернете. Поскольку нас интересует продолжительная работа, не было смысла заниматься овеклоком (разгонять процессор или видеокарту), по крайней мере по началу этой производительности точно хватало. К тому же почти любой разгон не только сильно повышает нагрев системы, но и влияет на вероятность вылетов вследствие случайных повреждений памяти, а к серверу предъявляются, наоборот, двойные стандарты по надежности. Поэтому в первую очередь нас интересуют температурные характеристики. Тест проводился с одной видеокартой Gigabyte GeForce RTX 3090 TURBO 24G, которая показала отличные температурные характеристики. При работе в стресс тесте видеокарты и 12 ядер процессора на неделю, температура видеокарты не поднималась выше 63 градусов, а процессора выше 59, что достойный показатель для игровых и умеренный для серверных систем. Ниже тест sysbench, для сравнения на моем домашнем ryzen 2600X total number of events: 121178. Когда тут, как на скриншоте ниже, 259501. Что более чем в два раза больше. При ровно в два раза большем количестве потоков. Причем стоящий дома ryzen еще и быстрее.

Что касается производительности RTX3090, пока еще рано говорить о ее рабочем потенциале, ибо наш суперский код, который создаст ваших 3D аватаров по фотографиям из instagram, еще не дописан. Однако если кому интересно она выдает где-то 110 Мега Хешей в секунду, что смешно по сравнению с любым асиком при ее стоимости на момент покупки она окупилась бы в майне за 314 дней (в день приносила бы почти 700р). Мораль не покупайте карты, чтобы майнить. Покупайте карты, чтобы играть, или учить искусственный интеллект. Чтобы он был умнее и посоветовал вам купить для майна ASIC.

Выводы

Собирай я сейчас бы тот же компьютер, наверное, поменял бы не так много. Советовал бы, как я писал выше, немного другую память, ибо когда мы выбирали свою еще было непонятно, какой будет процессор в конечной машине. Поменял бы скорее всего кулер, может, есть какие-то более удобные варианты. Хотя и качеством охлаждения я доволен. В будущих статьях возможно расскажу про настройку сервера. И удаленный GUI для нескольких пользователей. У вас есть предложения и замечания? Делитесь в комментариях!

Подробнее..

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!

Подробнее..

DALL E от OpenAi Генерация изображений из текста. Один из важнейших прорывов ИИ в начале 2021 года

06.01.2021 06:14:44 | Автор: admin

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

Итак, исследователи в области искусственного интеллекта из openai создали нейронную сеть под названием DALL E, которая генерирует изображения из текстового описания на естественном языке.

Если тебе интересно машинное обучение, то приглашаю вМишин Лернинг мой субъективный телеграм-канал об искусстве глубокого обучения, нейронных сетях и новостях из мира искусственного интеллекта.

DALL E представляет собой версиюGPT-3с 12 миллиардами параметров,обученную генерировать изображения из текстовых описаний на датасете из пар текст-изображение.Исследователи обнаружили, что DALL E обладает огромным репертуаром генеративных возможностей, включая возможность создания антропоморфных животных и других необычных объектов, комбинирующих совершенно нетривиальные свойства, например "кресло в форме авокадо."

Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"

Можно сказать, что уже были все предпосылки к созданию DALL E: прошлогодний триумф GPT-3 и успешное создание Image GPT сети, способной к генерации изображений на основе текста, использующей языковую модель трансформер GPT-2. Все уже подходило к тому, чтобы создать новую модель, взяв в этот раз за основу GPT-3. И теперь DALL E показывает невиданные доселе чудеса манипулирования визуальными концепциями с помощью естественного языка!

Как и GPT-3, DALL E это языковая модель-трансформер, принимающая на вход текст и изображение, как последовательность размером до 1280 токенов. Модель обучена максимизировать правдоподобие при генерации токенов, следующих один за другим.

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

Давайте посмотрим на примеры, которые говорят сами за себя. Исследователи утверждают, что не использовали ручной "cherry picking". Примерами являются изображения, полученные при помощи DALL E, в которых используются 32 лучших примера из 512-ти сгенерированных, отобранных созданным ранее (теми же openai) нейронным ранжированиемCLIP.

Text: a collection of glasses sitting on the table

Изображения, сгенерированные DALL EИзображения, сгенерированные DALL E

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

Text: an emoji of a baby penguin wearing a blue hat, red gloves, green shirt, and yellow pants

Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны

DALL E может не только генерировать изображение с нуля, но и регенерировать (достраивать) любую прямоугольную область существующего изображения, вплоть до нижнего правого угла изображения, в соответствии с текстовым описанием. В качестве примера за основу взяли верхнюю часть фотографии бюста Гомера. Модель принимает на вход это изображение и текст: a photograph of a bust of homer

Text: a photograph of a bust of homer

Фотография бюста ГомераФотография бюста Гомера

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

Text: a photo of phone from the ...

Фотографии телефонов разных десятилетий XX векаФотографии телефонов разных десятилетий XX века

Название модели DALL E является словослиянием имени художника Сальвадора Дали и робота WALL E от Pixar. Вышел такой своеобразный Вали-Дали. Вообще в мире ИИ "придумывание" таких оригинальных названий это некий тренд. Что определенно радует, и делает эту область еще более оригинальной.

Старый добрый перенос стиля WALL E в DalСтарый добрый перенос стиля WALL E в Dal

Для пущего сюрреализма и оправдания своего названия DALL E "попросили" сгенерировать животных, синтезированных из множества понятий, включая музыкальные инструменты, продукты питания и предметы домашнего обихода. Хотя это не всегда удавалось, исследователи обнаруживали, что DALL E иногда принимает во внимание формы двух объектов при решении о том, как их объединить. Например, когда предлагается нарисовать улитку-арфу.

Text: a snail made of harp

Улитка-Арфа. Фантастические твари и где они обитают..Улитка-Арфа. Фантастические твари и где они обитают..

Вывод

DALL E это декодер-трансформер, который принимает и текст, и изображение в виде единой последовательности токенов (1280 токенов = 256 для текста + 1024 для изображения) и далее генерирует изображения авторегрессивном режиме.

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

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

Что ты думаешь о DALL E и подобных генеративных нейронных моделях, способных создавать визуальный контент по текстовому описанию? Где может быть полезна такая технология? Насколько тебя впечатлили результаты? Давай обсудим в комментариях.

Подробнее..

Разбираемся, как подавить шум в речи с помощью глубокого обучения и OpenVINO

31.05.2021 12:16:05 | Автор: admin

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

Картинка со звукомКартинка со звуком

Задача шумоподавления с помощью глубокого обучения и OpenVINO попала в руки к студентам ITlab учебно-исследовательской лаборатории Университета Лобачевского при поддержке компании Intel. Студенты, начиная со 2 курса, под руководством преподавателей работают над интересными инженерными и научными проектами. Создание высокопроизводительного программного обеспечения требует применения специальных инструментов разработчика и технологий параллельного исполнения кода, и в рамках проектов лаборатории студенты с ними знакомятся. Данная статья является результатом работы студентов Вихрева Ивана, Рустамова Азера, Зайцевой Ксении, Кима Никиты, Бурдукова Михаила, Филатова Андрея.

Что есть звук в компьютере

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

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

Lizemijn Libgott / Vice.comLizemijn Libgott / Vice.com

Записанный звук состоит из множества звуковых волн, одновременно попадающих на датчик микрофона в некоторый промежуток времени, в результате чего мы получаем длинный вектор из чисел - это амплитуды (громкость) сигнала в течение небольшого времени. Частота сигнала проводного телефона 8kHz, это значит что мы за секунду 8000 раз измеряем амплитуду (громкость) суммарного сигнала, звуковые карты как правило используют частоту 44.1 или 48kHz.

На этой картинке 3 секунды звука или график на 120 тысяч значений. При таком сильном сжатии по оси X кажется, что мы закрасили площадь под графиком. На этой картинке 3 секунды звука или график на 120 тысяч значений. При таком сильном сжатии по оси X кажется, что мы закрасили площадь под графиком.

Воспроизвести аудиофайл в Python можно с помощью библиотек soundfile и sounddevice.

import sounddevice as sdimport soundfile as sfpath_wav = 'test_wav.wav'data, fs = sf.read(path_wav)sd.play(data, fs)status = sd.wait()

Запись данных с микрофона тоже происходит очень просто - посмотрите и запустите record.py.

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

Справа мы видим спектр - вклад каждой из волн в частотное разложение. Схема с сайта nuancesprog.ruСправа мы видим спектр - вклад каждой из волн в частотное разложение. Схема с сайта nuancesprog.ru

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

Пример спектрограммы, полученной из звукового файла, при помощи библиотеки Numpy.Пример спектрограммы, полученной из звукового файла, при помощи библиотеки Numpy.

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

def calcSpec(y, params, channel=None):     """compute complex spectrum from audio file"""     fs = int(params["fs"]) # Константа, обозначающая частоту дискредитации     # В нашем случае равна 16000 - наш wav файл записан с частотой 16 кГц    if channel is not None and (len(y.shape)>1):     # Если аудио содержит два канала (стерео) - берем только один канал         sig = sig[:,channel]     # STFT parameters     N_win = int(float(params["winlen"])*fs) # Расчёт размера окна Хэннинга     # В нашем случае 320     if 'nfft' in params:         N_fft = int(params['nfft'])     else:         N_fft = int(float(params['winlen'])*fs) # Расчёт ширины окна для преобразования Фурье         # В нашем случае 320     N_hop = int(N_win * float(params["hopfrac"])) # Расчёт прыжка для преобразования Фурье     # В нашем случае 160     win = np.sqrt(np.hanning(N_win)) # Окно Хэннинга      Y = stft(y, N_fft, win, N_hop)     return Y 

Функция Stft проводит преобразование Фурье. Массив делится на части определённой длины (рассчитанной в calcSpec) и для каждой из частей применяется функция преобразования Фурье, взятая из Numpy возвращает готовую спектрограмму.

def stft(x, N_fft, win, N_hop, nodelay=True):     """     short-time Fourier transform     x - Входной сигнал     N_fft - Количество точек, на которых используется преобразование     win - Окно Хэннинга     N_hop - Размер прыжка    nodelay - Удаление первых точек из конечного массива (В них появляется побочный эффект преобразования)     """     # get lengths     if x.ndim == 1:         x = x[:,np.newaxis] # Если подано несколько файлов, то создаётся дополнительная ось     Nx = x.shape[0] # Количество точек во входных данных (в нашем случае 160000)     M = x.shape[1] # Количество файлов во входных данных (в нашем случае 1)     specsize = int(N_fft/2+1)     N_win = len(win) # Размер окна Хэннинга     N_frames = int(np.ceil( (Nx+N_win-N_hop)/N_hop )) # На сколько частей делим входной массив     Nx = N_frames*N_hop # padded length     x = np.vstack([x, np.zeros((Nx-len(x),M))])      # init     X_spec = np.zeros((specsize,N_frames,M), dtype=complex) # Заполненная нулями матрица, которая станет спектрограммой     win_M = np.outer(win,np.ones((1,M))) # Создаём матрицу, в которой каждый столбец равен окну Хэннинга     x_frame = np.zeros((N_win,M)) # Заполненный нулями вектор (вектора в случае если на вход дали несколько файлов)     for nn in range(0,N_frames):         idx = int(nn*N_hop)         x_frame = np.vstack((x_frame[N_hop:,:], x[idx:idx+N_hop,:])) # Разделяем входной массив на куски размера N_hop         x_win = win_M * x_frame         X = np.fft.rfft(x_win, N_fft, axis=0) # Преобразование возвращает столбец комплексных, где действительная часть - амплитуда, а комплексная - фазовый сдвиг         X_spec[:,nn,:] = X # Добавляем полученный столбец в спектрограмму      if nodelay:         delay = int(N_win/N_hop - 1)         X_spec = X_spec[:,delay:,:] # Удаляем лишний столбец из начала      if M==1:         X_spec = np.squeeze(X_spec) # Удаляем лишнюю ось      return X_spec

Также важной функцией является calcFeat, позволяющая нам прологарифмировать спектрограмму, растягивая нижние частоты и сжимая верхние. Голос человека лежит в диапазоне 85-3000Гц, а диапазон звуковых частот в нашей записи 16кГц маленький промежуток на всем диапазоне, и помощью логарифмирования мы растягиваем нужные нам низкие частоты и поджимаем ненужные высокие

def calcFeat(Spec, cfg):     """compute spectral features"""     if cfg['feattype'] == "MagSpec":         inpFeat = np.abs(Spec)     elif cfg['feattype'] == "LogPow":         pmin = 10**(-12)         powSpec = np.abs(Spec)**2 # Все значения спектрограммы возводятся в квадрат         inpFeat = np.log10(np.maximum(powSpec, pmin)) # и логарифмируются с обрезанием слишком низких значений     else:         ValueError('Feature not implemented.')      return inpFeat 

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

def spec2sig(Spec, params):    """Конвертирует спектрограмму в звук"""    # частота дискретизации    fs = int(params["fs"])    # ширина окна     N_win = int(float(params["winlen"])*fs)    if 'nfft' in params:        N_fft = int(params['nfft'])    else:        # длина быстрого преобразования Фурье        N_fft = int(float(params['winlen'])*fs)    #длина сегментов окна    N_hop = int(N_win * float(params["hopfrac"]))    # окно Хеннинга    win = np.sqrt(np.hanning(N_win))    # обратное преобразование Фурье    x = istft(Spec, N_fft, win, N_hop)    return x

В istft обратное преобразование Фурье также выполняется при помощи функции взятой из Numpy.

def istft(X, N_fft, win, N_hop):    # get lengths    specsize = X.shape[0] # Спектрограмма    N_frames = X.shape[1] #  кол-во кадров    if X.ndim < 3:        X = X[:,:,np.newaxis] # Приведение размера до 3    M = X.shape[2] # кол-во каналов    N_win = len(win) # длина окна хеннинга    Nx = N_hop*(N_frames - 1) + N_win    # Умножение матрицы win и единичной матрицы размера 1,M    win_M = np.outer(win,np.ones((1, M)))     x = np.zeros((Nx,M))  # нулевая матрица Nx,M для сохранения ответа        for nn in range(0, N_frames):        X_frame = np.squeeze(X[:,nn,:]) # Вектор по данному фрейму        # обратное преобразование фурье для X_frame ,N_fft        x_win = np.fft.irfft(X_frame, N_fft, axis=0)                x_win = x_win.reshape(N_fft,M) # изменяем размер        # получаем окно хеннинга нужного размера         x_win = win_M * x_win[0:N_win,:]        # добавляем результат для данного фрейма        idx1 = int(nn*N_hop); idx2 = int(idx1+N_win)        x[idx1:idx2,:] = x_win + x[idx1:idx2,:]         if M == 1:        x = np.squeeze(x) # Убираем лишние измерения если канал один         return x

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

Речь до и после удаления шумаРечь до и после удаления шума

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

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

В нашем случае мы воспользуемся моделью NSNet2. Эта нейронная сеть использовалась в Deep Noise Suppression Challenge, проводимом компанией Microsoft. Целью разработки данной сети было создание модели для очистки звука от шума в реальном времени. Данная модель состоит из полносвязного слоя с ReLU, двух рекуррентных GRU (Gated Recurrent Unit) блоков и полносвязных слоев (FF, feed forward) с ReLU и sigmoid активацией.

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

Представленные результаты по качеству работы можно посмотреть в статье Data augmentation and loss normalization for deep noise suppression. Построенная модель имеет хорошие показатели для различных типов шума.

Конвертация модели в OpenVINO

При работе с нашей моделью мы использовали OpenVINO (Open Visual Inference & Neural Network Optimization) - продукт, разрабатываемый компанией Intel. Как видно из названия, OpenVINO - это набор инструментов для исполнения и оптимизации нейронных сетей.

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

Мы берем обученную модель в каком-либо фреймворке, конвертируем в OpenVINO и теперь можем запустить хоть на CPU, хоть на iGPU или dGPU, хоть на FPGAМы берем обученную модель в каком-либо фреймворке, конвертируем в OpenVINO и теперь можем запустить хоть на CPU, хоть на iGPU или dGPU, хоть на FPGA

По факту, Model Optimizer - это набор python-скриптов, которые позволяют привести нейронные сети различных форматов к некоторому универсальному представлению, называемому IR (Intermediate Representation). Это позволяет OpenVINO работать с любой нейросетью, независимо от того, из какого фреймворка она взята.

В процессе своей работы Model Optimizer также оптимизирует структуру сверточных нейронных сетей. Например, объединяя результаты сверток, заменяя слои на последовательность линейных операций и т.д.

В последнее время, с появлением API, в Model Optimizer проводится все меньше оптимизаций, и основная его работа сводится к конвертации моделей без каких-либо серьезных изменений.

Конвертация в IR-представление различается для моделей из Open Model Zoo и других моделей. Open Model Zoo репозиторий глубоких нейросетевых моделей, содержащий большое количество обученных моделей, которые могут исполняться при помощи OpenVINO. Данный репозиторий хранит не только модели, но и параметры для конвертации моделей из разных фреймворков в промежуточный формат OpenVINO.

Для конвертации моделей, загруженных из Open Model Zoo, нужно воспользоваться инструментом Model Optimizer и входящим в него скриптом converter.py. Данный модуль имеет доступ к параметрам конвертации моделей из зоопарка моделей.

Консольная команда для конвертации загруженной модели:

python converter.py --name <имя модели> --download_dir <путь до папки, в которую скачали модель>   

Чтобы сконвертировать собственную модель, необходимо использовать скрипт mo.py с дополнительными параметрами:

python mo.py --input_model <путь до модели> --output_dir <путь до папки, в которую поместить конвертированную модель> --input_shape <размеры входа модели>  

Для конвертации нашей ONNX модели в формат OpenVINO (в Windows) вышеприведенная команда выглядит так:

python mo.py --input_model <путь до папки с моделью>\nsnet2-20ms-baseline.onnx -output_dir <путь до папки, в которую поместить конвертированную модель> --input_shape [1, 1000, 161]   

где 1 - количество каналов, 1000 - временных интервалов, 161 - частот.

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

python mo.py --help

Отличие converter.py от mo.py лишь в том, что converter.py использует параметры для конвертации из описания модели в Open Model Zoo и передает их в mo.py


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

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

Подробнее..

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

10.12.2020 18:18:39 | Автор: admin
Доктор Джино Каспари (справа) во время геофизических исследований королевской скифской гробницы на юге Сибири в 2018 году. Фото: Тревор Уоллес

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

Джино Каспари, археолог-исследователь из Швейцарского национального научного фонда, изучает культуру древних скифов и кочевников, которые терроризировали население равнин Азии 3000 лет назад. В гробницах скифской знати хранится большая часть сказочных богатств, украденных ими у соседей. С того момента, как тела вождей предавали земле, могилы становились мишенями для грабителей. По оценкам доктора Каспари, более 90% из них уже уничтожены и разорены.

Ученый подозревает, что тысячи гробниц разбросаны по евразийским степям, простирающимся на миллионы квадратных километров. Он часами занимался картированием захоронений, используя изображения Google Earth на территории современной России, Монголии и провинции Синьцзян в Западном Китае.

На самом деле, это довольно скучная и однообразная работа, рассказывает доктор Каспари. И это явно не то, чем должен заниматься высокообразованный ученый.

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

Пабло и Джино были коллегами по International House (всемирная сеть из 160 языковых школ и педагогических институтов более чем в 50 странах). Их объединяла вера в важность общедоступности знаний и академического сотрудничества. Еще они оба любили хеви-метал. За кружкой пива они дали старт научному партнерству и открыли новую страницу в истории археологических исследований.

Доктор Каспари часами занимался картированием скифских захоронений на огромной территории современной России, Монголии и Китая с использованием изображений Google Earth. Фото: Пабло Креспо

Изображения гробниц, которые использовали Пабло Креспо и доктор Каспари для обучения нейронной сети. Фото: Пабло Креспо

Сверточная нейронная сеть (convolutional neural network, CNN) идеально подходит для анализа фотографий и других изображений. CNN видит изображение как сетку из пикселей. Нейронная сеть, разработанная Пабло Креспо, начинает с того, что присваивает каждому пикселю рейтинг в зависимости от его цвета насколько он красный, зеленый или синий. После оценки каждого пикселя в соответствии с множеством дополнительных параметров сеть начинает анализировать небольшие группы пикселей, затем более крупные, ища совпадения с данными, которые она была обучена обнаруживать.

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

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

По словам доктора Креспо, создать сеть было несложно. Он развернул ее менее чем за месяц, используя Python, без лишних затрат. Если, конечно, не считать купленное и выпитое за этот месяц пиво. Доктор Каспари надеется, что СNN поможет археологам находить новые гробницы, чтобы их можно было защитить от охотников за сокровищами.

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

С помощью подобной технологии Netflix дает нам рекомендации по фильмам, говорит Пабло Креспо, ныне старший научный сотрудник компании Etsy. Почему бы нам не использовать ее для чего-то вроде сохранения человеческой истории?

Габриэле Гаттилья и Франческа Аничини, археологи из Пизанского университета в Италии, проводят раскопки в районе памятников эпохи Римской империи, что влечет за собой анализ тысяч битых кусков керамики. В римской культуре почти все типы посуды, включая инвентарь для приготовления пищи и амфоры, используемые для перевозки товаров по Средиземному морю, были сделаны из глины. Поэтому анализ керамики важен для понимания жизни древних римлян.

Слева доктор Франческа Аничини. Справа Габриэле Гаттилья. Источник: Пизанский университет, MAPPALab

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

Задача заключается в сравнении черепков глиняной посуды с изображениями в печатных каталогах. По оценкам доктора Гаттиглиа и доктора Аничини, только 20% их времени уходит на раскопки. Остальное тратится на анализ гончарного дела работу, за которую им не платят.

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

Эта мечта вылилась в проект ArchAIDE цифровое решение, которое позволит археологам сфотографировать найденную керамику в полевых условиях и идентифицировать ее с помощью нейросетей. В проекте, получившем финансирование проекта Horizon 2020, теперь участвуют исследователи со всей Европы, а также группа ученых-информатиков из Тель-Авивского университета в Израиле, которые и разработали нейронную сеть.

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

Я мечтаю о каталоге всех видов керамики, сказал доктор Аничини. Но кажется, это работа не на одну жизнь.

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

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

Исследователи запускают автономный подводный аппарат с побережья Мальты. Фото: доктор Зои Вуд / Колледж Харви Мадда.

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

Шон Грэм, профессор в области digital humanities в Карлтонском университете в Оттаве, использует в работе нейросеть под названием Inception 3.0. CNN, разработанная Google, помогает искать через изображения в интернете объявления о покупке или продаже человеческих костей. В Соединенных Штатах и многих других странах действуют законы, требующие, чтобы человеческие кости, хранящиеся в музейных коллекциях, возвращались потомкам владельцев костей. Но есть люди, которые нарушают этот закон. Доктор Грэм сказал, что он даже находил в интернете видео о людях, копающих могилы, чтобы насытить черный рынок.

Он внес некоторые изменения в сеть Inception 3.0, чтобы она могла распознавать фотографии человеческих костей. Система уже была обучена распознавать объекты на миллионах изображений, но ни один из этих объектов не был костями. С тех пор он обучил свою нейросеть на более чем 80 000 изображений человеческих костей. Сейчас ученый взаимодействует с организацией под названием Противодействие преступности в Интернете, которая использует нейронные сети для отслеживания изображений, связанных с незаконной торговлей слоновой костью и секс-рабством.

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

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

Подробнее..

Перевод FermiNet квантовая физика и химия с азов

25.02.2021 00:07:45 | Автор: admin


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

Исследователи смогут прототипировать новые материалы и соединения in silico прежде, чем попытаться синтезировать их в лаборатории. Также выложен код из этого исследования; таким образом, команды специалистов по вычислительной физике и химии могут опираться на проделанную работу и применять ее при решении разнообразных проблем. В рамках исследования была разработана новая архитектура нейронной сети, Fermionic Neural Network или FermiNet, которая хорошо подходит для моделирования квантового состояния больших совокупностей электронов а ведь именно на электронах основаны все химические связи. Сеть FermiNet впервые продемонстрировала, как использовать глубокое обучение для вычисления энергии атомов и молекул с азов. Полученная модель оказалась достаточно точной для практического применения и на момент публикации оригинала статьи (октябрь 2020) оставалась наиболее точным нейросетевым методом, применяемым в отрасли. Предполагается, что связанные с ней методы и инструментарий могут пригодиться при решении фундаментальных проблем в естественных науках. Авторы FermiNet уже применяют ее в работе над сверткой белков, динамикой стеклообразных соединений, квантовой хромодинамикой на решетке и во многих других проектах, помогающих воплотить данные наработки на практике.

Краткая история квантовой механики


Упомянув квантовую механику, вы, скорее всего, озадачите собеседника этой темой как никакой иной. Сразу вспоминаются такие образы, как кот Шрёдингера, который парадоксально может быть одновременно жив и мертв, а также элементарные частицы, которые одновременно являются и корпускулами, и волнами. В квантовой системе такая частица как электрон не имеет конкретного местоположения, в отличие от ситуации в классической физике. В квантовой физике позиция электрона описывается облаком вероятностей то есть, размазана по всем тем точкам, в каждой из которых может оказаться электрон. Из-за такого абсурдного состояния вещей Ричард Фейнман счел возможным заявить: Думаю, я смело могу сказать, что квантовой механики никто не понимает.

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

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

Головокружительный оптимизм тех дней красиво сформулировал Поль Дирак:
Итак, базовые физические законы, необходимые для математической теории, которая бы описывала большую часть физики и всю химию, уже известны. Загвоздка в том, что на практике применение этих законов дает слишком сложные уравнения, решить которые нам объективно не под силу. Поэтому представляется желательным разработать приблизительные методы для практического применения квантовой механики.
1929

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



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

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

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

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

Фермионные нейронные сети


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

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

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

Поэтому потребовалось разработать нейронную сеть нового типа, которая была бы антисимметрична относительно поступающего в нее ввода. Мы назвали ее Fermionic Neural Network или FermiNet. В большинстве квантовохимических методов антисимметрия вводится при помощи функции, именуемой детерминантом. Детерминант это матрица, обладающая следующим свойством: если поменять местами два ее ряда, то вывод умножается на -1, точно, как волновая функция фермионов. Можно взять набор одноэлектронных функций, рассчитать их для каждого электрона в вашей системе, а затем уложить все результаты в одну матрицу. В таком случае детерминант матрицы будет подлинно антисимметричной волновой функцией. Основное ограничение данного подхода заключается в том, что результирующая функция именуемая слэтеровский детерминант не слишком широко применима. Волновые функции реальных систем, как правило, гораздо сложнее. Как правило, для исправления этой проблемы берутся большие линейные комбинации слэтеровских детерминантов иногда миллионы и более после чего в них вносятся некоторые простые поправки, на основе пар электронов. Даже после этого система может оказаться недостаточно точна для расчета энергий.



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

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

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



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

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

В то время как большинство нейронных сетей обучаются на некоторых внешних данных, в нашем случае нейронная сеть сама генерирует тот ввод, который поступает в нее для обучения. Ситуация немного напоминает вытягивание самого себя за волосы из трясины и означает, что нам не требуется никаких учебных данных кроме позиций тех атомных ядер, вокруг которых пляшут электроны. Базовая идея, известная под названием вариационный квантовый метод Монте-Карло (или VMC для краткости) известен в науке с 1960-х и, как правило, считается дешевым, но не очень точным способом расчета энергии системы. Заменив простые волновые функции, основанные на слэтеровских детерминантах, на функции из FermiNet, удалось радикально повысить точность такого подхода на всех рассмотренных нами системах.



Рисунок 4 Смоделированные электроны, выбранные из FermiNet, движутся вокруг молекулы бициклобутана.

Чтобы убедиться, что FermiNet действительно является прорывом в своей предметной области, мы начали с исследования простых, хорошо изученных систем, например, атомов из первого ряда периодической таблицы (от водорода до неона). Это небольшие системы 10 электронов или менее поэтому они поддаются исследованию при помощи наиболее точными (но усложняющимися по экспоненте) методами. FermiNet намного превосходит сравнимые расчеты VMC, и зачастую позволяет сократить ошибку по сравнению с экспоненциально масштабируемыми расчетами наполовину и более. В более крупных системах методы, усложняющиеся по экспоненте, становятся неприменимы, поэтому в качестве отсчетного мы использовали метод связанных кластеров. Этот метод хорошо работает на молекулах со стабильными конфигурациями, но буксует, когда связи оказываются растянуты или повреждены, а такие факторы критически важны для понимания химических реакций. Притом, что он масштабируется гораздо лучше, чем по экспоненте, тот метод связных кластеров, который был применен в описанном исследовании, всем равно работает как максимум с молекулами средних размеров. Мы применяли FermiNet ко все более крупным молекулам, начиная с гидрида лития и дойдя до бициклобутана это была самая крупная система, которую мы рассмотрели, в ней 30 электронов. На самых мелких молекулах FermiNet улавливала поразительные 99,8% разницы между энергией связанных кластеров и энергией, получаемой от единственного слэтеровского детерминанта. В случае с бициклобутаном, FermiNet все равно улавливала 97% или более этой корреляционной энергии огромное достижение для якобы дешевого, но неточного подхода.



Рисунок 5 графическое представление той доли корреляционной энергии, которую FermiNet верно улавливает при работе с молекулами. Пурпурная планка отмечает показатель в 99% корреляционной энергии. Слева направо: гидрид лития, азот, этилен, озон, этанол и бициклобутан.

Тогда как методы связанных кластеров хорошо работают со стабильными молекулами, настоящий передний край вычислительной химии связан с пониманием того, как молекулы растягиваются, скручиваются и рвутся. При решении таких задач методы связных кластеров часто сбоят, поэтому приходится сравнивать результат с как можно более многочисленными контрольными образцами, чтобы убедиться в непротиворечивости полученного ответа. В рамках описанного опыта были рассмотрены две контрольные растянутые системы молекула азота (N2) и цепочка водорода из 10 атомов (H10). В молекуле азота особенно сложная связь, поскольку от каждого атома в ней участвует по 3 электрона.

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

Заключение


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

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

Перевод Как экономить память и удваивать размеры моделей PyTorch с новым методом Sharded

07.01.2021 18:21:18 | Автор: admin
Модели глубокого обучения улучшаются с увеличением количества данных и параметров. Даже с последней моделью GPT-3 от Open AI, которая использует 175 миллиардов параметров, нам ещё предстоит увидеть плато роста количества параметров.

Для некоторых областей, таких как NLP, рабочей лошадкой был Transformer, который требует огромных объёмов памяти графического процессора. Реалистичные модели просто не помещаются в памяти. Последний метод под названием Sharded [букв. сегментированный] был представлен в Zero paper Microsoft, в котором они разработали метод, приближающий человечество к 1 триллиону параметров.

Специально к старту нового потока курса по Machine Learning, делюсь с вами статьей о Sharded в которой показывается, как использовать его с PyTorch сегодня для обучения моделей со вдвое большей памятью и всего за несколько минут. Эта возможность в PyTorch теперь доступна благодаря сотрудничеству между командами FairScale Facebook AI Research и PyTorch Lightning.





Для кого эта статья?


Эта статья предназначена для всех, кто использует PyTorch для обучения моделей. Sharded работает на любой модели, независимо от того, какую модель обучать: NLP (трансформатор), зрительную (SIMCL, swav, Resnet) или даже речевые модели. Вот моментальный снимок прироста производительности, который вы можете увидеть с помощью Sharded во всех типах моделей.



SwAV это современный метод контролируемого данными обучения в области компьютерного зрения.
DeepSpeech2 это современный метод для речевых моделей.
Image GPT передовой метод для визуальных моделей.
Трансформер передовой метод обработки естественного языка.

Как использовать Sharded вместе с PyTorch


Для тех, у кого не так много времени, чтобы прочитать интуитивно понятное объяснение о том, как работает Sharded, я сразу объясню, как использовать Sharded с вашим кодом PyTorch. Но призываю прочитать конец статьи, чтобы понять, как работает Sharded.

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

Самый простой способ зарядить ваш код с помощью Sharded это преобразовать вашу модель в PyTorch Lightning (это всего лишь рефакторинг). Вот 4-минутное видео, которое показывает, как преобразовать ваш код PyTorch в Lightning.



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



Если ваша модель взята из другой библиотеки глубокого обучения, она всё равно будет работать с Lightning (NVIDIA Nemo, fast.ai, Hugging Face). Всё, что вам нужно сделать, это импортировать модель в LightningModule и начать обучение.

from argparse import ArgumentParserimport torchimport torch.nn as nnimport pytorch_lightning as plfrom pytorch_lightning.metrics.functional import accuracyfrom transformers import BertModelclass LitBertClassifier(pl.LightningModule):    def __init__(self, n_classes, pretrained_model_name='bert-base-uncased'):        super().__init__()        self.save_hyperparameters()        self.bert = BertModel.from_pretrained(pretrained_model_name)        self.drop = nn.Dropout(p=0.3)        self.out = nn.Linear(self.bert.config.hidden_size, n_classes)        self.loss_fn = nn.CrossEntropyLoss()    def forward(self, input_ids, attention_mask):        outputs = self.bert(            input_ids=input_ids,            attention_mask=attention_mask,            return_dict=False        )        pooled_output = outputs[1]        output = self.drop(pooled_output)        return self.out(output)    def training_step(self, batch, batch_idx):        loss, acc = self._shared_step(batch, batch_idx)        self.log("acc", acc)        return loss    def validation_step(self, batch, batch_idx):        _, acc = self._shared_step(batch, batch_idx)        self.log("val_acc", acc)    def _shared_step(self, batch, batch_idx):        input_ids = batch["input_ids"]        attention_mask = batch["attention_mask"]        targets = batch["targets"]        outputs = self.forward(            input_ids=input_ids,            attention_mask=attention_mask        )        _, preds = torch.max(outputs, dim=1)        loss = self.loss_fn(outputs, targets)        acc = accuracy(preds, targets)        return loss, acc    def configure_optimizers(self):        return torch.optim.AdamW(self.parameters(), lr=2e-5)if __name__ == '__main__':    # TODO: add your own dataset    train_dataloader = ...    val_dataloader = ...    bert = LitBertClassifier()    trainer = pl.Trainer(gpus=8, plugins='ddp_sharded')    trainer.fit(bert, train_dataloader)

Интуитивно понятное объяснение работы Sharded


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


Обучение DP

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

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


Параллельное распределение данных

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

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

Использование какого-либо распределённого режима




В PyTorch Lightning переключение режимов распределения тривиально.

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

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

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

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

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

Подробнее..

Перевод Как удалить татуировку с помощью глубокого обучения

20.04.2021 16:13:49 | Автор: admin

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


Запуск Google Colab

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

https://colab.research.google.com/

После запуска войдите в свою учётную запись Google. Если вы уже вошли, платформа просто выберет основную учётную запись для входа. Не беспокойтесь! Ваши данные здесь в безопасности. После входа перейдите к файлу и откройте новую записную книжку.

Клонирование репозитория GitHub

Теперь в только что созданной записной книжке мы должны выполнить такую команду:

!git clone https://github.com/vijishmadhavan/SkinDeep.git SkinDeep
Эта команда клонирует код GitHub в вашу среду Colab.Эта команда клонирует код GitHub в вашу среду Colab.

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

cd SkinDeep

Установка библиотек

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

!pip install -r colab_requirements.txt

Определение архитектуры модели

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

import fastaifrom fastai.vision import *from fastai.utils.mem import *from fastai.vision import open_image, load_learner, image, torchimport numpy as npimport urllib.requestimport PIL.Imagefrom io import BytesIOimport torchvision.transforms as Tfrom PIL import Imageimport requestsfrom io import BytesIOimport fastaifrom fastai.vision import *from fastai.utils.mem import *from fastai.vision import open_image, load_learner, image, torchimport numpy as npimport urllib.requestimport PIL.Imagefrom io import BytesIOimport torchvision.transforms as Tclass FeatureLoss(nn.Module):    def __init__(self, m_feat, layer_ids, layer_wgts):        super().__init__()        self.m_feat = m_feat        self.loss_features = [self.m_feat[i] for i in layer_ids]        self.hooks = hook_outputs(self.loss_features, detach=False)        self.wgts = layer_wgts        self.metric_names = ['pixel',] + [f'feat_{i}' for i in range(len(layer_ids))              ] + [f'gram_{i}' for i in range(len(layer_ids))]    def make_features(self, x, clone=False):        self.m_feat(x)        return [(o.clone() if clone else o) for o in self.hooks.stored]        def forward(self, input, target):        out_feat = self.make_features(target, clone=True)        in_feat = self.make_features(input)        self.feat_losses = [base_loss(input,target)]        self.feat_losses += [base_loss(f_in, f_out)*w                             for f_in, f_out, w in zip(in_feat, out_feat, self.wgts)]        self.feat_losses += [base_loss(gram_matrix(f_in), gram_matrix(f_out))*w**2 * 5e3                             for f_in, f_out, w in zip(in_feat, out_feat, self.wgts)]        self.metrics = dict(zip(self.metric_names, self.feat_losses))        return sum(self.feat_losses)        def __del__(self): self.hooks.remove()

Загрузка файла модели

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

MODEL_URL = "https://www.dropbox.com/s/vxgw0s7ktpla4dk/SkinDeep2.pkl?dl=1"urllib.request.urlretrieve(MODEL_URL, "SkinDeep2.pkl")path = Path(".")learn=load_learner(path, 'SkinDeep2.pkl')

Входное изображение

Наконец, можно определить своё входное изображение для тестирования. В приведённом ниже сегменте кода подставьте URL-адрес изображения.

url = 'https://images.pexels.com/photos/5045947/pexels-photo-5045947.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260' #@param {type:"string"}response = requests.get(url)img = PIL.Image.open(BytesIO(response.content)).convert("RGB")img_t = T.ToTensor()(img)img_fast = Image(img_t)show_image(img_fast, figsize=(8,8), interpolation='nearest');

Тестирование модели и получение результатов

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

p,img_hr,b = learn.predict(img_fast)Image(img_hr).show(figsize=(8,8))

Заключение

Вот и всё. Мы обсудили пошаговое реальное применение модели SkinDeep для удаления татуировок с кожи. Подобные забавы лишь малая демонстрация потенциала глубокого обучение. Оно способно способно генерировать новые функции без вмешательства человека, из ограниченного набора функций, расположенных в наборе учебных данных. Для специалистов это означает, что они могут использовать более сложные наборы функций по сравнению с традиционным ПО для машинного обучения. Если вас заинтересовала эта сфера ждем вас на расширенном курсе Machine Learning и Deep Learning, в котором мы совместили изучение DL с классическим курсом по ML, чтобы студент начал с основ и постепенно перешел к более сложным вещам.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

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

Перевод Как обучать огромные модели машинного обучения на случайных GPU

11.01.2021 14:11:20 | Автор: admin
Вы можете спросить: почему эти полумагические модели машинного обучения работают так хорошо? Короткий ответ: эти модели чрезвычайно сложны и обучаются на огромном количестве данных. На самом деле, Lambda Labs недавно подсчитала, что для обучения GPT-3 на одном GPU потребовалось бы 4,6 миллиона долларов если бы такое было возможно.

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

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





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

Эксперименты показывают, что систему на базе BERT можно за день обучить с помощью более чем 8 GPU, большинство из которых нам пришлось позаимствовать в неработающих лабораториях. Прежде чем мы представим HetSeq, нужна небольшая предыстория.

Типовое обучение нейронной сети


def train(args):    # main components    dataset = Dataset()    dataloader = DataLoader(dataset)    model = Model()    loss_ = Loss()    optimizer = Optimizer(model.parameters())    # specify the GPU and transfer model to the GPU     device = Device()     model.to(device)    model.train()        # actual training loops    for epoch in range(1, Max_Epoch):        for (data, target) in dataloader:            data, target = data.to(device), target.to(device)   # **load input data and target to GPU**            optimizer.zero_grad()            output = model(data)    # **forward compute the output of model given input data**            loss = loss_(output, target)   # **forward process to compute the real loss function**            loss.backward()    # **backward process to obtain the**            optimizer.step()    # **update parameters** 

Обучение на одном GPU

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

Фактически процесс обучения состоит из четырёх отдельных этапов: (1) загрузка данных, (2) прямой проход, (3) обратный проход, (4) обновление.

1. Загрузка данных


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


Прямой проход с одним GPU

2. Прямой проход


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

3. Обратный проход


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


Параметры обновления с единственным GPU

4. Обновление


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

Краткое описание этапов обучения


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

Что делать, если у нас несколько GPU?


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

def torch.nn.parallel.DistributedDataParallel(    module,  # pre-defined model    device_ids=None, # input device_ids    output_device=None,  # output device_ids, in our case, input device = output device = single GPU    dim=0,     broadcast_buffers=True, # set to False in our implementation    process_group=None, # Core part    bucket_cap_mb=25,     find_unused_parameters=False,     check_reduction=False)view raw

Класс параллельного распределения данных

Это не новая идея. В PyTorch мы используем для модели модуль torch.nn.parallel.DistributedDataParallel (DDP) вместо модуля torch.nn.Module. Каждый GPU это отдельный процесс, и связь между ними осуществляется с помощью стандартного IPC. Но это ещё не всё. Четыре шага требуют некоторой настройки.

1. Загрузка данных с помощью DDP


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

Это основная идея параллельного распределения данных (DDP): каждый GPU имеет идентичные параметры модели, но одновременно обрабатывает разные пакеты данных.


Прямой проход с несколькими GPU

2. Прямой проход с DDP


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

3. Обратный проход с DDP


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


Синхронизация градиента

4. Обновление с DDP


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

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

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

def train_multiple_GPUs(args, device_id):    # main components    dataset = Dataset()    dataloader = DataLoader(dataset)    model = DDP(Model())    loss_ = Loss()    optimizer = Optimizer(model.parameters())    device = Device(device_id)     model.to(device)    model.train()        # actual training loops    for epoch in range(1, Max_Epoch):        for (data, target) in dataloader:            data, target = data.to(device), target.to(device)              optimizer.zero_grad()            model.synchronization()    #  parameter synchronization            output = model(data)                loss = loss_(output, target)            loss_average = average(loss)            loss.backward()            model.parameter.grad.average()            optimizer.step()

Обучение на нескольких GPU

Масштабирование несколько узлов с несколькими GPU


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

def torch.distributed.init_process_group(            backend=args.distributed_backend,    # 'nccl' is the best available backend for GPU            init_method=args.distributed_init_method,    # 'tcp' or shared file system            world_size=args.distributed_world_size,  # number of nodes in total            rank=args.distributed_rank, # index of current node        )

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

Коммуникация вот где возникают сложности



Внутриузловая и межузловая коммуникация

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

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

Когда родители заставляют вас делиться игрушками


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

  1. Некоторые игрушки имеют сложные инструкции. Распределённая параллельная обработка данных пакета (DDP) это боль, трудно понять её и заставить работать. Особенно верно это для большинства исследователей машинного обучения, которые не очень хорошо разбираются в особенностях распределённых вычислений. В дополнение к базовой настройке DDP мирный тренировочный запуск различных архитектур GPU на многих узлах требует тщательного разделения данных и изнурительного налаживания связи между GPU и узлами.
  2. С какими-то игрушками лучше играть лучше, чем с другими. В гетерогенной системе некоторые GPU работают быстрее других, а некоторые имеют больше памяти, чем у других. Это означает, что какие-то процессоры получают больше данных для обработки, чем другие, что прекрасно; но это также означает, что средние значения градиентов и обновления параметров должны тщательно взвешиваться.
  3. Родители не разрешают нам играть с какими-то игрушками. Большинство существующих распределённых обучающих платформ GPU требуют дополнительных пакетов, таких как Docker, OpenMPI и т. д. К сожалению, большинство компетентных администраторов кластеров не позволяют пользователям иметь административные привилегии, необходимые для настройки каждого узла, чтобы обучить модель.
  4. Какие-то игрушки плохо работают с другими. Пакеты глубокого обучения, такие как BERT и GPT2/3, разработанные крупными компаниями, как правило, имеют определённые форматы дизайна модели с несколькими логическими слоями, что затрудняет их использование и адаптацию к приложению.

Из-за этих проблем мы создали общую систему, которая охватывает все сложные части DDP: разделение данных, совместимость и настраиваемость, и развернули эту систему в Нотр-Даме.

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

BERT в университете с HetSeq


Начнём с Anaconda. Создадим виртуальную среду и установим Python.

$ conda create --name hetseq$ conda activate hetseq$ conda install python=3.7.4


Затем мы установим пакеты и привязки HetSeq: загрузим HetSeq с GitHub, установим пакеты из requirements.txt, а также HetSeq и биндинги из setup.py.
$ git clone https://github.com/yifding/hetseq.git $ cd /path/to/hetseq $ pip install -r requirements.txt $ pip install --editable .

Последний шаг перед обучением это загрузка файлов данных BERT, включая корпус обучения, конфигурацию модели и словарь BPE отсюда. Загрузите DATA.zip, распакуйте его и поместите в каталог preprocessing/.

Обучение BERT с помощью HetSeq


Крутая вещь в HetSeq: она абстрагирует все детали о распределённой обработке. Таким образом, код обучения для 100 GPU почти такой же, как для одного! Давайте попробуем!

$DIST=/path/to/hetseq $python3 ${DIST}/train.py  \ $       --task bert   --data ${DIST}/preprocessing/test_128/ \ $       --dict ${DIST}/preprocessing/uncased_L-12_H-768_A-12/vocab.txt  \ $       --config_file ${DIST}/preprocessing/uncased_L-12_H-768_A-12/bert_config.json  \ $       --max-sentences 32  --fast-stat-sync --max-update 900000 --update-freq 4  \ $       --valid-subset test --num-workers 4 \ $       --warmup-updates 10000  --total-num-update 1000000 --lr 0.0001  \ $       --weight-decay 0.01 --distributed-world-size 1  \ $       --device-id 0 --save-dir bert_single_gpu

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

На первом узле:

$DIST=/path/to/hetseq $python3 ${DIST}/train.py  \ $       --task bert   --data ${DIST}/preprocessing/test_128/ \ $       --dict ${DIST}/preprocessing/uncased_L-12_H-768_A-12/vocab.txt  \ $       --config_file ${DIST}/preprocessing/uncased_L-12_H-768_A-12/bert_config.json  \ $       --max-sentences 32  --fast-stat-sync --max-update 900000 --update-freq 4  \ $       --valid-subset test --num-workers 4 \ $       --warmup-updates 10000  --total-num-update 1000000 --lr 0.0001  \ $       --weight-decay 0.01 --save-dir bert_node2gpu4  \ $       --distributed-init-method tcp://10.00.123.456:11111 \ $       --distributed-world-size 8 --distributed-gpus 4 --distributed-rank 0

На втором узле:

$DIST=/path/to/hetseq $python3 ${DIST}/train.py  \ $       --task bert   --data ${DIST}/preprocessing/test_128/ \ $       --dict ${DIST}/preprocessing/uncased_L-12_H-768_A-12/vocab.txt  \ $       --config_file ${DIST}/preprocessing/uncased_L-12_H-768_A-12/bert_config.json  \ $       --max-sentences 32  --fast-stat-sync --max-update 900000 --update-freq 4  \ $       --valid-subset test --num-workers 4 \ $       --warmup-updates 10000  --total-num-update 1000000 --lr 0.0001  \ $       --weight-decay 0.01 --save-dir bert_node2gpu4  \ $       --distributed-init-method tcp://10.00.123.456:11111 \ $       --distributed-world-size 8 --distributed-gpus 4 --distributed-rank 4

Два блока кода работают на двух разных узлах. Адрес TCP/IP должен быть установлен как один из IP-адресов узла. Как только они будут запущены, вы сможете наблюдать за выполнением кода на 8 процессорах и 2 разных узлах!

Так насколько хорошо это работает? Мы провели несколько экспериментов (подробности тут) над различными однородными (гомогенными, hom) и неоднородными (гетерогенными, het) установками.

nodes GPUs training_time speed_up
1 4 7.19day 1.00
2(het) 8 4.19day 1.72
2(hom) 8 4.26day 1.69
4(het) 16 2.23day 3.22
4(hom) 16 2.19day 3.28
8(het) 32 1.21day 5.94

В общей сложности мы смогли управлять 32 GPU в 8 неоднородных узлах, сокращая время обучения языковой модели BERT с семи дней до примерно одного дня.

Под капотом HetSeq



Структура пакета HetSeq

Пакет HetSeq содержит три основных модуля, показанных на рисунке слева: train.py, task.py и controller.py для координации основных компонентов, показанных справа. Модуль train.py инициализирует распределённую систему и её различные компоненты.

Модуль task.py определяет функции модели, набора данных, загрузчика данных и оптимизатора; он также выполняет функции прямого и обратного распространения. Модуль controller.py действует как главный контроллер обучения. Он работает как модель, оптимизатор и планировщик скорости обучения; загружает и сохраняет чекпоинты, сообщает о потере и обновляет параметры.
Но я хочу обучить не BERT!

Но я хочу обучить не BERT!


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

image



Подробнее..

Ограничения репрезентативной способности и оценка ошибки обобщения для графовых нейронных сетей

19.12.2020 18:18:04 | Автор: admin

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

Работа направлена на исследование двух проблем, связанных с графовыми нейронными сетями. Во-первых, авторы приводят примеры различных по структуре графов, но неразличимых и для простых, и для более мощных GNN. Во-вторых, они ограничивают ошибку обобщения для графовых нейронных сетей точнее, чем VC-границы.

Введение

Графовые нейронные сети - это модели, которые работают напрямую с графами. Они позволяют учитывать информацию о структуре. Типичная GNN включает в себя небольшое число слоев, которые применяются последовательно, обновляя представления вершин на каждой итерации. Примеры популярных архитектур: GCN, GraphSAGE, GAT, GIN.

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

a_v^{t+1} = AGG \left(h_w^t: w \in \mathcal{N}\left(v\right)\right) \\ h_v^{t+1} = COMBINE \left(h_v^t, a_v^{t+1}\right),

где AGG - обычно функция, инвариантная к перестановкам (sum, mean, max etc.), COMBINE - функция объединяющая представление вершины и ее соседей.

Дерево вычислений для двухслойной GNN на примере узла A. Источник: https://eng.uber.com/uber-eats-graph-learning/Дерево вычислений для двухслойной GNN на примере узла A. Источник: https://eng.uber.com/uber-eats-graph-learning/

Более продвинутые архитектуры могут учитывать дополнительную информацию, например, фичи ребер, углы между ребрами и т.д.

В статье рассматривается класс GNN для задачи graph classification. Эти модели устроены так:

  1. Сначала производятся эмбеддинги вершин с помощью L шагов графовых сверток

  2. Из эмбеддингов вершин получают представление графа с помощью инвариантной к перестановкам функций (например, sum, mean, max)

  3. Классификатор предсказывает бинарную метку по представлению графа

Ограничения графовых нейронных сетей

В статье авторы рассматривают ограничения для трех классов GNN:

  • Локально инвариантные графовые нейронные сети (LU-GNN). Сюда входят вышеупомянутые GCN, GraphSAGE, GAT, GIN и другие

  • CPNGNN, которая вводит локальный порядок, нумеруя соседние вершины от 1 до d, где d - степень вершины (в оригинальной работе это называют port numbering)

  • DimeNet, которая оперирует 3D-графами, учитывая расстояния между вершинами и углы между ребрами

Ограничения LU-GNN

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

Ограничения CPNGNN

Однако, существует плохой порядок, при котором и CPNGNN произведет одинаковые эмбеддинги для вершин одного цвета.

Граф S8 и две копии S4 различаются в таких характеристиках, как обхват, окружность (длина наименьшего и наибольшего циклов соответственно), радиус, диаметр и количество циклов, но CPNGNN с таким порядком портов и readout-ом, инвариантным к перестановкам, произведет для одинаковые эмбеддинги. А значит, по ним невозможно будет восстановить правильные характеристики для обоих графов.

Здесь CPNGNN с такими же условиями не сможет восстановить характеристики из предыдущего примера для графа G2 и двух копий G1. Однако, DimeNet сможет это сделать, так как она учитывает углы, которые не везде совпадают в случае этих графов, например, \angle A_1B_1C_1 и \angle \underline{A}_1\underline{B}_1\underline{C}_1 .

Ограничения DimeNet

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

Шаг в сторону более мощных GNN

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

GNN, предложенная авторами работает так:

  1. Шаг DimeNet

  2. Дополнение message-эмбеддинга ребра m_{uv}^{\left(l\right)} геометрической информацией \Phi_{uv} по формуле \underline{m}_{uv}^{\left(l\right)} = \underline{f}\left(m_{uv}^{\left(l\right)}, \Phi_{uv}\right)

  3. Получение эмбеддинга вершины с учетом введенного локального порядка \left(c_v\left(i\right), t_{i, v}\right) , где c - i-ый сосед вершины v, t - эмбеддинг порядка.

    Формула обновления представления вершины:

    h_{v}^{\left( l + 1 \right)} = f \left( h_{v}^{\left( l \right)}, \underline{m}_{c_v\left( 1 \right)v}^{\left( l \right )}, t_{1, v}, ..., \underline{m}_{c_v\left( d \left( v \right ) \right)v}^{\left( l \right )}, t_{ d \left( v \right ), v} \right )

  4. Шаг инвариантного к перестановкам readout-а

  5. Классификатор или регрессор в зависимости от задачи

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

Оценка ошибки обобщения

Авторы рассматривают конкретный класс моделей: LU-GNN, в которой обновление представления вершины происходит согласно

h_v^{l + 1} = \phi \left( W_1x_v + W_2 \rho \left( \sum_{u \in \mathcal{N} \left( v \right)} g\left( h_u^l \right)\right) \right),

где \phi,\ g,\ \rho - нелинейные функции активации, x_v - вектор признаков для вершины v, такие, что \rho \left(0\right) = 0,\ \forall v: \lVert x_v \rVert_2 \le B_x,\ \forall x \in \mathbb{R}^r: \lVert \phi \left( x \right ) \rVert_{\infty} \le b < \infty,\ \phi\left( 0 \right ) = 0,\ g\left( 0 \right ) = 0 . Также, \phi,\ g,\ \rho липшицевы с константами C_{\phi},\ C_{g},\ C_{\rho} соответственно, а \lVert W_1 \rVert_2 \le B_1,\ \lVert W_2 \rVert_2 \le B_2 . W_1,\ W_2,\ \phi,\ g,\ \rho одинаковы на всех слоях GNN.
Далее бинарный классификатор применяется к представлению каждой вершины отдельно и полученные метки усредняются. Параметр \beta бинарного классификатора обладает свойством \lVert \beta \rVert_2 \le B_{\beta} .

Пусть f\left(G\right) - результат применения GNN к графу с меткой y \in \{0, 1\} , p\left( f \left( G \right ), y \right ) = y \left( 2f \left( G \right ) - 1 \right ) + \left( 1 - y \right ) \left( 1 - 2 f \left( G \right ) \right ) - разница между вероятностями правильного и неправильного лейбла, p\left( f \left( G \right ), y \right ) < 0 только в случае ошибки классификации.

Фукнция потерь, где a = -p\left( f \left( G \right ), y \right ) , \mathbb{I}\left[\right] - индикаторная функция:

loss_{\gamma}\left( a \right ) = \mathbb{I}\left[ a > 0\right ] + (1 + \frac{a}{\gamma})\mathbb{I}\left[ a \in \left[ \gamma, 0 \right ] \right].

Тогда эмпирическая сложность Радемахера для класса функций предсказания GNN f на тренировочном множестве \{G_j, y_j\}_{j=1}^m :

\hat{\mathcal{R}}\left( f \right) = \frac{1}{m} \sum_{j = 1}^m loss_{\gamma} \left( -p\left( f \left(G_j\right), y_j \right) \right)

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

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

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

  • Малые изменения в весах модели так же вносят малый вклад в изменение класса (доказательство приведено в статье)

Это позволяет вывести границы ошибки обобщения по аналогии с RNN

Cравнение с RNN, видно сходствоCравнение с RNN, видно сходство

В таблице \mathcal{C} - сложность перколяции: \mathcal{C} = C_{\phi}C_gC_{\rho}B_2 , r - размер эмбеддинга, d - максимальная степень вершины, m - размер тренировочной выборки, L - количество слоев, \gamma - зазор, зависящий от данных

Эти оценки позволяют существенно более точно оценить ошибку обобщения, по сравнению с работой, где получили оценку \tilde{\mathcal{O}} \left(r^3N / \sqrt{m} \right)

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

Выводы

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

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

Подробнее..

Как я преподавал курс AIMLDL от Samsung

09.12.2020 20:22:50 | Автор: admin
Всем привет. Расскажу вам про свой взгляд на ИИ, так сказать, изнутри процесса. В смысле образовательного и научного процесса.

Так сложилось что в 1998 я поступил аспирантуру в РГАСХМ и темой своей научной работы выбрал AI/ML. Это были суровые времена очередного ледникового периода нейронных сетей. Как раз в это время Ян Лекун опубликовал свою знаменитую работу Gradient-Based Learning Applied to Document Recognition о принципах организации сверточных сетей, которая, на мой взгляд, как раз и была началом новой оттепели. Забавно, что тогда я работал над некоторыми похожими элементами, верно ведь говорят, что идея, когда приходит её время, носится в воздухе. Однако не всем дано ее воплотить в жизнь. Свою работу я, к сожалению, так и не довел до защиты, но всегда хотел когда-нибудь закончить ее.


Источник: Hitecher

И вот по прошествии 20 лет, когда я стал работать преподавателем в Южном Федеральном Университете и одновременно преподавать в программе дополнительного образования IT Школа Samsung, мне представился второй шанс. Компания Samsung предложила ЮФУ первому запустить учебный трек IT Академии Samsung по искусственному интеллекту для бакалавров и магистров. У меня были некоторые опасения в том, что удастся реализовать всю учебную программу в полном объеме, но я с энтузиазмом откликнулся на предложение читать курс. Я понял, что круг замкнулся, и мне представился таки второй шанс сделать то, что когда-то не удалось. Здесь нужно отметить, что курс Samsung AI/ML это один из лучших на сегодня открытых русскоязычных курсов, доступных бесплатно на платформе Stepik (https://stepik.org/org/srr). Однако в случае ВУЗовской программы, помимо теоретико/практического курса, добавлялась проектная часть. То есть годовая учебная программа IT Академии Samsung считалась освоенной в случае изучения двух модулей Нейронные сети и компьютерное зрение, Нейронные сети и обработка текста с получением соответствующих сертификатов Stepik, а также выполнения индивидуального проекта. Обучение по курсу завершалось защитой проектов студентов, на которую приглашались эксперты, в т.ч. сотрудники московского Центра искусственного интеллекта Samsung.

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

  • линейной алгебры,
  • теории вероятности,
  • дифференциального исчисления,
  • языка программирования Python.

Конечно, все требуемые знания и умения не выходят за рамки учебной программы бакалавриата 3-го курса ВУЗа. Приведу пару примеров, из тех что посложнее:

  • Найдите производную функции активации гиперболического тангенса и выразите результат через $th(x)$.
  • Найдите производную функции активации сигмоиды и выразите результат через сигмоиду $(x)$.
  • В графе вычислений приведенном на рис. 1 представлена сложная функция $y$ с параметрами $b1,b2,c1,c2$. Для удобства добавлены промежуточные результаты операций как $z_1z_9$. Необходимо определить, чему будет равна производная $y$ по параметру $b1$




Честно говоря, кое-что, особенно из современных алгоритмов работы с нейронными сетями, я спешно изучал вместе со студентами. Изначально предполагалось, что студенты будут сами изучать видео-лекции онлайн курса Samsung на Stepik, а в аудитории мы будем делать только практикумы. Однако, я принял решение также читать и теорию. Это решение обусловлено тем, что с преподавателем можно разобрать непонятную тему, обсудить возникшие идеи и т.д. Практические же задачи студенты получали в виде домашних заданий. Подход оказался правильным на занятиях получалась живая атмосфера, я видел, что студенты в целом достаточно успешно осваивают материал.

Через месяц мы плавно перешли от модели нейрона к первым простым полносвязным архитектурам, от простой регрессии к многоклассовой классификации, от простого вычисления градиента к алгоритмам оптимизации градиентного спуска SGD, ADAM и т.д. Завершали первую половину курса сверточные сети и современные архитектуры глубоких сетей. Финальным заданием первого модуля по Computer Vision было участие в соревновании на Kaggle "Dirty vs Cleaned" с преодолением порога точности в 80%.

Еще один, на мой взгляд, важный фактор: мы не были замкнуты внутри вуза. Организаторы трека проводили для нас вебинары и мастер-классы с приглашенными экспертами из лабораторий Samsung. Такие мероприятия повышали мотивацию студентов, да и мою, честно говоря :). Например, было интересное профориентационное мероприятие онлайн-мост между аудиториями ЮФУ, МГУ и Samsung, на котором сотрудники московского Центра ИИ Samsung рассказали о современных направлениях развития AI/ML и ответили на вопросы студентов.

Вторая часть курса, посвященная обработке текста, начиналась с общей теории лингвистического анализа. Далее студентам были представлены векторная и TF-IDF модели текста, а потом и дистрибутивная семантика и word2vec. По результатам было проведено несколько интересных практикумов: генерация ембедингов word2wec, генерация имен и лозунгов. Дальше мы перешли к теории и практике по использованию сверточных и рекуррентных сетей для анализа текста.

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

  1. Мониторинг физической активности человека Благодарный Александр, Крикунов Станислав.
  2. Разработка алгоритмов идентификации усталости человека Соленова Антонина.
  3. Распознавание маски на лице Будаева Екатерина.
  4. Распознавание эмоций по фотографии лица Пандов Вячеслав.
  5. Определение эмоций по речи Тихонов Алексей.
  6. Косметологический консультант Жмайлова Наталья.
  7. Цена слова Ринкон Диас Хаиро Алонсо, Моралес Кастро Хайме Игнасио.



Все проекты прошли защиту, но степень сложности и проработанности была разной, что, вполне справедливо, нашло отражение в оценках за проекты. По результатам защиты четыре проекта были отобраны на ежегодный конкурс IT Академии Samsung. И с гордостью могу сказать, что жюри присудили двум нашим проектам высшие места. Ниже я приведу краткое описание этих проектов, основанное на материалах, предоставленных моими студентами Благодарным Александром, Крикуновым Станиславом и Пандовым Вячеславом, за что им большое спасибо. Считаю, что продемонстрированные ими решения вполне могут быть оценены как серьезная исследовательская работа.

I место в номинации Искусственный интеллект конкурса IT Академии Samsung.
Мониторинг физической активности человека, Благодарный Александр, Крикунов Станислав


Проект состоял в том, чтобы создать мобильное приложение, определяющее и количественно подсчитывающее физическую активность на тренировках при помощи сенсоров мобильного телефона. Сейчас существует множество мобильных приложений, которые могут распознавать физическую активность человека: Google Fit, Nike Training Club, MapMyFitness и другие. Однако, эти приложения не могут распознавать отдельные виды физических упражнений и подсчитывать количество повторений.
Один из авторов проекта Благодарный Александр, мой выпускник по программе IT Школа Samsung 2015 года, и я не без гордости, радовался, что полученные еще в школе знания по мобильной разработке были применены в таком ключе.


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

Датасет был собран авторами самостоятельно. При выполнении 7 различных упражнений использовались 3 вида смартфонов (Android версии 4.4 ,9.0, 10.0). Смартфон закреплялся на руке с помощью специального кармана. Суммарно тремя добровольцами было выполнено 1800 повторений. При выполнении могли возникать ошибки в технике по каким-либо причинам, поэтому была проведена процедура очистки выборки. Для этого были построены распределения взаимных корреляций для всех видов упражнений. Затем для каждого упражнения был выбран порог корреляции, ниже которого упражнение считается негодным и исключается из выборки.

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

Для нейросети было решено применять данные в частотной области. Поскольку масса тела человека достаточно велика, можно ожидать, что на большинстве стандартных упражнений характерные частоты сигнала будут лежать в низкочастотной области спектра. При этом высокие частоты можно считать либо дрожью при выполнении, либо шумами датчиков. Что это значит? Это значит, что мы можем найти спектр сигнала при помощи быстрого преобразования Фурье и использовать только 10-20 % данных для анализа. Почему так мало? Так как 1) спектр симметричен, можно сразу отсечь половину составляющих 2) основная информация лишь 20-40% информативной части спектра. Такие предположения особенно хорошо описывают медленные силовые упражнения.


Нормированный временной ряд для разных упражнений


Нормированный спектр для разных упражнений

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

Нейросеть выполняет задачу классификации упражнений. Это значит, что она выдает вектор вероятностей всех упражнений из списка, по которым она была обучена. Индекс максимального элемента в этом векторе номер выполненного упражнения. При этом, если уверенность в выполненном упражнении менее 85%, то считается, что ни одно из упражнений не было выполнено. Сеть состоит из 3 слоев: 4 свёрточных, 3 полносвязных, количество выходных нейронов равно количеству упражнений, которые мы хотим распознавать. В архитектуре для экономии вычислительных ресурсов используются только свёртки с размером ядра 3х3. Сравнительно простая архитектура сети оправдана ограниченными вычислительными ресурсами смартфонов, в нашей задаче необходимо распознавание с минимальной задержкой.


Описание архитектуры нейросети

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

Результаты: при более-менее качественном выполнении упражнений уверенность сети составляет 95-99%. На валидационной выборке точность составила 99.8%.


Ошибка при обучении на валидационной выборке


Матрица ошибок для нейросети

Нейросеть была встроена в мобильное приложение и показала аналогичные результаты, как и при обучении.

В ходе исследования также были опробованы другие модели машинного обучения, используемые в наши дни для решения задач классификации: логистическая регрессия, Random forests, XG Boost. Для этих архитектур были использованы регуляризация Тихонова (L2), перекрёстная проверка и gridsearch для поиска оптимальных параметров. Показатели точности в результате оказались следующими:

  • Логистическая регрессия: 99.4 %
  • Random forests: 99.1 %
  • XG Boost: 97.5%

Знания, полученные в ходе обучения в IT Академии Samsung, помогли авторам проекта расширить горизонты интересов и оказали неоценимый вклад при поступлении в магистратуру Сколковского института науки и технологий. На данный момент мои студенты занимаются там исследованиями в области машинного обучения для систем связи.

Код на GitHub

II место номинации Искусственный интеллект конкурса IT Академии Samsung.
Распознавание эмоций по фотографии лица, Пандов Вячеслав




Работа модели хорошо описана на этом слайде:



Все начинается с фотографии. В представленной реализации оно приходит от Telegram-бота. По нему Dlib frontal_face_detector находит все лица на изображении. Затем с помощью Dlib shape_predictor_68_face_landmarks детектируются 68 ключевых 2D точек каждого лица. Каждый набор нормализуется следующим образом: центрируется (вычитая среднее по X и Y) и масштабируется (деля на абсолютный максимум по X и Y). Каждая координата нормализованной точки принадлежит интервалу [-1, +1].

Затем в дело вступает нейросеть, которая предсказывает глубину каждой ключевой точки лица координату Z, используя нормализованные координаты (X, Y). Данная модель обучалась на датасете AFLW2000.

Далее эти точки соединяются между собой, образуя сеточную маску. Её ещё можно назвать биометрией лица. Длины отрезков такой маски используются как один из способов определения эмоций. Идея в том, что каждый отрезок имеет своё место в векторе отрезков и некоторые из них в зависимости от эмоции. И каждая эмоция, теоретически, имеет ограниченное количество таких векторов. Эта гипотеза подтвердилась в процессе экспериментов. Для обучения такой модели использовались датасеты: Cohn-Kanade+, JAFFE, RAF-DB.

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

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

Среди аналогичных решений можно выделить следующие: EmoPy, DLP-CNN (RAF-DB), FER2013, EmotioNet. Однако сложно делать сравнения, т.к. обучались они на разных данных.

Код на GitHub

Заключение


В заключение хочется сказать, что пилотный курс показал свою состоятельность, и в этом 2020/21 учебном году программа уже преподается в 23х ВУЗах партнерах IT Академии Samsung в России и Казахстане. Полный список можно увидеть здесь. В это году у нас уже учится группа магистров и бакалавров (в группе есть даже один целый к.э.н.!) и пока в основной массе успешно грызет гранит науки. Идеи на индивидуальный проект пока еще только предстоит найти, но студенты полны оптимизма. Конечно в следующем конкурсе индивидуальных проектов конкуренция возрастет десятикратно, но мы надеемся и в дальнейшем получать высокие оценки достижений наших учеников. И самое главное, я уверен, что полученные знания и опыт будут очень серьезным подспорьем для наших выпускников в их дальнейшем развитии в сфере IT.

2020 г. Ростов-на-Дону. ЮФУ, IT Академия Samsung.


Дмитрий Яценко
Старший преподаватель кафедры информационных и измерительных технологий, факультет высоких технологий, Южного Федерального Университета,
Преподаватель IT Школы Samsung,
Преподаватель трека AI IT Академии Samsung.
Подробнее..

Recovery mode Инструменты для участников соревнований по машинному обучению

13.02.2021 22:06:51 | Автор: admin

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


Преимущества, которые получают организаторы соревнований:


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

И участники соревнований также получают пользу:


  • Публичное признание высокой квалификации
  • Денежные призы
  • И просто удовольствие от участия и победы

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


Приступим!


Determined


Платформа для тренировки моделей глубокого обучения.


  • Ускоренное обучение моделей, с помощью state-of-the-art распределенного обучения, без изменения кода модели
  • Автоматический поиск высококачественных моделей, с расширенной настройкой гипер-параметров от создателей Hyperband
  • Умное планирование использования своих GPU и сокращение расходов на облачные GPU, за счет использования вытесняемых инстансов
  • Отслеживание и воспроизводство экспериментов, включая версии кода, показатели, контрольные точки и гипер-параметры
  • Легкость интеграции с популярными DL-фреймворками
  • Позволяет больше времени тратить на создание моделей, чем на управление инфраструктурой

Compose


Инструмент машинного обучения для автоматизированного прогнозирования.


  • Структурирование задач прогнозирования и создание меток для обучения с учителем
  • Поиск обучающих примеров исходя из конечного желаемого результата, заданного функцией разметки
  • Передача результата в Featurepools для автоматизированного проектирования признаков
  • Передача результата в EvalML для автоматизированного машинного обучения

Featuretools


Фреймворк для автоматизированного проектирования признаков.


  • Преобразование временных и реляционных наборов данных в матрицы признаков
  • Возможность автоматически генерировать описания признаков на английском языке

EvalML


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


  • В сочетании с Featuretools и Compose позволяет создавать end-to-end ML-решения для обучения с учителем

Pandas Profiling


Создает отчеты профиля из DataFrame Pandas.


  • Вместо df.describe() функция df.profile_report()
  • Быстрый анализ данных
  • Интерактивный HTML-отчет со столбцами
  • Вывод типа: определение типов
  • Основы: тип, уникальные значения, отсутствующие значения
  • Квантильные статистические данные: минимум, Q1, медиана, Q3, максимум, диапазон, межквартильный размах
  • Описательная статистика: среднее, мода, стандартное отклонение, сумма, среднее абсолютное отклонение, коэффициент вариации, эксцесс, асимметрия
  • Наиболее частые значения
  • Гистограмма
  • Корреляции сильно зависимых переменных: матрицы Спирмана, Пирсона и Кендалла
  • Матрица пропущенных значений: количество, тепловая карта и дендрограмма
  • Анализ текста: категории (прописные буквы, пробел), кодировка (латиница, кириллица) и блоки (ASCII) в текстовых данных
  • Анализ файлов и изображений: размеры файлов, даты создания, усеченные изображения и изображения, содержащие EXIF

Tpot


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


  • Автоматизирует самую утомительную часть машинного обучения, интеллектуально исследуя тысячи возможных пайплайнов, чтобы найти лучшие из ваших данных
  • После завершения поиска предоставляет код Python для лучшего найденного пайплайна
  • Сделан на основе Scikit-learn

Shap


Теоретико-игровой подход к объяснению результатов любой ML-модели.


  • Имеет точный алгоритм для ансамбля деревьев
  • Может использоваться в моделях глубокого обучения

Feature-engine


Библиотека с множественными трансформерами фичей для использования ML-моделях.


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

Lale


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


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

Biome


Инструмент для работы с неструктурированными данными.


  • Автоматическая классификация короткие и шумные тексты, длинные тексты; инструменты мониторинга и анализа результатов классификации; простой в использовании пользовательский интерфейс аннотаций; предварительно сконфигурированные и расширяемые классификаторы
  • Извлечение данных табличные данные, длинные документы; встроенные готовые объекты (дата, время, количество, вес, размер, единицы измерения), поддержка нескольких форматов файлов (PDF, Word, Excel, HTML, E-mail или простой текст); настраиваемые объекты, атрибуты и отношения; реляционный вывод объектов, отношений, ролей и атрибутов на основе графов знаний
  • Сравнение настраиваемые сервисы семантического сходства для предложений, абзацев и текстового контента в базах данных; аналитические пользовательские интерфейсы для поиска наиболее похожих и непохожих элементов

DataSketch


Инструмент для вероятностных структур данных.


  • обработка и поиск больших объемов данных очень быстро
  • очень маленькая потеря точности

PyTextRank


Инструмент для работы с текстом.


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

Joblib


Набор инструментов для легкого создания пайплайнов.


  • Простые параллельные вычисления
  • Прозрачное кэширование функций и ленивая переоценка
  • Оптимизирован для быстрой и надежной обработки больших данных и массивов
  • Удобный повторный перезапуск экспериментов
  • Отделение логики выполнения потока от логики предметной области и кода
  • Параллельный помощник упрощение написания читаемого параллельного кода и его отладки
  • Замена Pickle для работы с объектами, содержащими большие данные

Shampoo


Алгоритм предварительной обработки с учетом структуры.


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

Michelangelo


Платформа машинного обучения от Uber.


  • Обеспечение непрерывного рабочего процесса
  • Централизованное хранилище функций
  • Распределенная инфраструктура обучения
  • Оценка и визуализация моделей с деревьями решений
  • Средства развертывания моделей
  • Прогнозирование и маршрутизация
  • API для подключения конвейеров

Hasty.ai


Инструмент для создания меток изображений.


  • Быстрая разметка данных
  • Автоматизация процесса разметки
  • Обучение помогающей модели прямо во время разметки
  • Поиск вероятных ошибок

Cortex


Инструмент для крупномасштабных рабочих нагрузок.


  • Развертывание моделей в качестве API реального времени или пакетного
  • Высокая доступность с зонами доступности и автоматическим перезапуском экземпляров
  • Логический вывод экземпляров по запросу или спотовых экземпляров с резервными копиями по запросу
  • Автомасштабирование для обработки производственных рабочих нагрузок с поддержкой избыточного выделения запросов

Weights & Biases


Набор инструментов для машинного обучения.


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

SpeedRun


Набор инструментов для развертывания и управления ML-экспериментами.


  • Чтение файлов конфигурации и управление каталогами экспериментов
  • Логирование в Weights & Biases
  • Настройка и запуск гипер-параметров с помощью Weights & Biases
  • Запись текста или изображений в файл, индикаторы выполнения
  • Преобразование фигур matplotlib в изображения
  • Визуализация многомерных изображений
  • Ожидание завершения запущенных процессов и освобождения ресурсов

Great Expectations


Работа с данными тестирование, документирование и профилирование.


  • Автоматическое документирование данных
  • Генерирование документации из тестов
  • Автоматическое профилирование данных

Keras Tuner


Платформа для для оптимизации гипер-параметров.


  • Определение пространства поиска
  • Поиск наилучших значений
  • Встроенные алгоритмы байесовской оптимизации

NanoEdge AI Studio


Десктопное приложение для AI-библиотек, предназначенное для разработчиков встроенных приложений и MCU C кода.


  • Поиск лучших библиотек для встроенных проектов
  • Включение возможности машинного обучения в MCU C код
  • Запуск библиотек на любых Arm Cortex-M микроконтроллерах и оптимизированных для них
  • Очень маленький размер памяти модели (1-20kB RAM/Flash)
  • Ультра быстрые модели (1-20ms вывод на M4 80MHz)
  • Автоматическая проверка качества данных
  • Автоматический поиск лучшей AI модели
  • Сбор и импорт данных через последовательный порт в реальном времени
  • Эмулятор для тестирования библиотеки перед встраиванием
  • Простота развертывания C библиотек
  • Модели могут обучены напрямую, без использования MCU
  • Для создания и развертывания моделей не требуется опыт и экспертиза в ML

LabelBox


End-to-end платформа для создания и управления высококачественными данными.


  • Автоматизированная разметка
  • Общее рабочее пространство для работы с данными и коллективного взаимодействия внутренних и внешних команд
  • Отслеживание активности и прогресса работы
  • Управление доступом и ролями
  • API (Python, GraphQL) и SDK
  • Работа с изображениями: классификация, распознавание и сегментация
  • Работа с видео: производительный редактор видео, метки на видео до 30 FPS с уровнем кадра, аналитика признаков меток
  • Работа с текстом: классификация, распознавание именованных сущностей, поддержка сложных онтологий с встроенными классификациями
  • Предварительная маркировка на основе моделей и активного обучения
  • Приоритизация очереди маркировки наиболее важных данных с помощью API

LabelML


Организация ML-экспериментов и мониторинг процесса обучения с мобильного.


  • Легкая интеграция (2 строчки кода)
  • Хранение лога экспериментов, включая гит-коммитs, настройки и гипер-параметры
  • Хранение лога Tensorboard
  • Панель управления в локальном браузере
  • Хранение контрольных точек
  • API для настраиваемой визуализации

PyCaret


Low-code ML-библиотека.


  • Быстрый процесс от подготовки данных до деплоинга модели
  • Фокусировка на бизнес-задачах вместо кодинга
  • Легкость использования и построения полного процесса эксперимента
  • Анализ производительности модели (более 60 графиков)
  • Подготовка данных (недостающие значения, трансформинг категориальных данных, создание признаков, настройка гипер-параметров модели)
  • Поддержка алгоритма Боруты

CometML


Инструмент для быстрого создания моделей


  • Отслеживание, сравнение, объяснение и оптимизация экспериментов и моделей
  • Быстрая интеграция
  • Сравнение экспериментов код, гипер-параметры, метрики, предсказания, зависимости, системные метрики
  • Отладка моделей просмотр, анализ, получение информации и визуализация данных
  • Рабочее пространство для взаимодействия команды

ClearML


Решение для объединения ML-инструментов (MLOps).


  • Один набор инструментов для автоматизации подготовки, выполнения и анализа экспериментов
  • Управление экспериментами параметры, задания, артефакты, метрики, отладочные данные, метаданные и логи
  • Управление и оркестровка GPU/CPU ресурсов, автоматическое масштабирование на облачных и локальных машинах
  • Хранилище данных версионирование анализа; создание и автоматизация пайплайнов данных; ребалансировка, смешивания и сочетания датасетов

Благоприятная обстановка


Создает комфорт, удобство, приятность, душевность и способствует творческому вдохновению


  • Комната с приятной обстановкой
  • Классическая музыка
  • Хорошее настроение

Заключение.


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


Вперед, к победам!

Подробнее..

Как построить свою систему поиска похожих изображений

04.04.2021 14:14:58 | Автор: admin

Представлюсь

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

Эта публикация предназначена для Machine Learning инженеров и написана по мотивам моего выступления Поиск похожих изображений - справочник от А до Я, который был опубликован сообществом Open Data Science на Data Fest Online 2020.

Данная статья содержит справочную информацию по зарекомендованным методам, применяемым в задаче Image Retireval. Прочитав статью, вы сможете построить систему поиска похожих изображений под вашу задачу с нуля (не включая процесс разработки production решения).

О задаче

Поиск похожих изображений (по-другому, Content-Based Image Retrieval или CBIR) - это любой поиск, в котором участвуют изображения.

Проще всего о задаче расскажет картинка сверху из статьи Recent Advance in Content-based Image Retrieval: A Literature Survey (2017).

Сейчас все активнее применяется подход "Поиск по фото", в частности, в e-commerce сервисах (AliExpress, Wildberries и др.). "Поиск по ключевому слову" (с пониманием контента изображений) уже давно осел в поисковых движках Google, Яндекс и пр., но вот до маркетплейсов и прочих частных поисковых систем еще не дошел. Думаю, с момента появления нашумевшего в кругах компьютерного зрения CLIP: Connecting Text and Images ускорится глобализация и этого подхода.

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

Базовые компоненты сервиса

Шаг 1. Обучение модели. Модель может быть сделана на классике CV или на базе нейронной сети. На вход модели - изображение, на выход - D-мерный дескриптор/эмбеддинг. В случае с классикой это может быть комбинация SIFT-дескриптора + Bag of Visual Words. В случае с нейронной сетью - стандартный бэкбон по типу ResNet, EfficientNet и пр. + замысловатые пулинг слои + хитрые техники обучения, о которых мы далее поговорим. Могу сказать, что при наличии достаточного объема данных или хорошего претрена нейронные сети сильно выиграют почти всегда (мы проверяли), поэтому сосредоточимся на них.

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

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

Нейросети и Metric Learning

Нейронная сеть в задаче поиска похожих используется как feature extractor (бэкбон). Выбор бэкбона зависит от объема и сложности данных - рассмотреть можно все от ResNet18 до Visual Transformer.

Первая особенность моделей в Image Retrieval - это магия в голове нейросети. На лидерборде по Image Retrieval борются за построение лучших дескрипторов - тут есть и Combined Global Descriptors с параллельными пулингами и Batch Drop Block для более равномерного распределения активации по выходной карте признаков.

Второй главной фишкой являются функции ошибок. Их очень много. Только в Deep Image Retrieval: A Survey представлено больше десятка зарекомендованных парных лоссов. Еще столько же есть классификационных. Главная суть всех этих лоссов - обучить нейросеть трансформировать изображение в вектор линейно разделимого пространства, так чтобы далее можно было сравнивать эти вектора по косинусному или евклидову расстоянию: похожие изображения будут иметь близкие эмбеддинги, непохожие - далекие. Рассмотрим подробнее.

Функции ошибок

Contrastive Loss

Самая простая для понимания функция ошибки - Contrastive Loss. Это парный лосс, т.е. объекты сравниваются по расстоянию между друг другом.

Нейросеть штрафуется за отдаленность друг от друга эмбеддингов изображений p и q, если эти изображения на самом деле похожи. Аналогично, возникает штраф за близость эмбеддингов, изображения которых на самом деле непохожи друг на друга. При этом в последнем случае мы ставим границу m (например, 0.5), преодолев которую, мы считаем, что нейросеть справилась с задачей "разъединения" непохожих изображений.

Triplet Loss

Triplet Loss берет во внимание три объекта - якорь, позитив (похожий на якорь) и негатив (отличный от якоря). Это также парный лосс.

Здесь мы нацелены на минимизацию расстояния от якоря до позитива и максимизацию расстояния от якоря до негатива. Впервые Triplet Loss был представлен в статье FaceNet от Google по распознаванию лиц и долгое время был state-of-the-art решением.

N-tupled Loss

N-tupled Loss - развитие Triplet Loss, в котором также берется якорь и позитив, но вместо одного негатива используется несколько негативов.

Angular Additive Margin (ArcFace)

Проблема парных лоссов заключается в выборе комбинаций позитивов, негативов и якорей - если их просто брать равномерно случайными из датасета, то возникнет проблема "легких пар". Это такие простые пары изображений, для которых лосс будет 0. Оказывается, сеть достаточно быстро сходится к состоянию, в котором большинство элементов в батче будут для нее "легкими", и лосс для них окажется нулевым - сеть перестанет учиться. Чтобы избежать этой проблемы, стали придумывать изощренные техники майнинга пар - hard negative и hard positive mining. Подробнее о проблеме можно почитать в этой статье. Существует также библиотека PML, в которой реализовано множество методов майнинга, да и вообще в библиотеке представлено много полезного по задаче Metric Learning на PyTorch.

Еще одним решением проблемы являются классификационные лоссы. Рассмотрим одну популярную функцию ошибки, которая привела к state-of-the-art в распознавании лиц три года назад - ArcFace.

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

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

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

Пулинги

Вернемся к архитектуре нейросети и рассмотрим парочку pooling слоев, применяемых в задачах Image Retrieval

R-MAC

Regional Maximum Activation of Convolutions (R-MAC) - пулинг слой, принимающий выходную карту нейронной сети (до глобального пулинга или слоев классификации) и возвращающий вектор-дескриптор, посчитанный как сумма активаций в различных окнах выходной карты. Здесь активацией окна является взятие максимума по этому окну для каждого канала независимо.

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

GeM

Generalized Mean (GeM) - простой пулинг, который может улучшить качество выходного дескриптора. Суть в том, что классический average pooling можно обобщить на lambda-норму. При увеличении lambda мы заставляем сеть фокусироваться на значимых частях изображения, что в определенных задачах может быть важно.

Ранжирование

Индексы

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

Самое простое - сохранить "в лоб" эмбеддинги и делать brute-force поиск по ним, например, с помощью косинусного расстояния. Проблемы появляются тогда, когда эмбеддингов становится много - миллионы, десятки миллионов или еще больше. Скорость поиска значительно снижается, объем занимаемой динамической памяти увеличивается. Одна позитивная вещь остается - это качество поиска, оно идеально при имеющихся эмбеддингах.

Датасет glove, размер эмбеддинга 100, расстояние - angularДатасет glove, размер эмбеддинга 100, расстояние - angular

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

Самые популярные: отечественная NMSLIB, Spotify Annoy, Facebook Faiss, Google Scann. Также, если хочется взять индексирование с REST API "из коробки", можно рассмотреть приложение Jina.

Переранжирование

Исследователи в области Information Retrieval давно поняли, что упорядоченная поисковая выдача может быть улучшена неким способом переупорядочивания элементов после получения исходной выдачи.

Одним из таких методов является Query Expansion. Идея состоит в том, чтобы использовать top-k ближайших элементов для генерации нового эмбеддинга. В самом простом случае можно взять усредненный вектор, как показано на картинке выше. Также можно взвесить эмбеддинги, например, по отдаленности в выдаче или косинусному расстоянию от запроса. Подобные улучшения описаны в едином фреймворке в статье Attention-Based Query Expansion Learning. По желанию можно применить Query Expansion рекурсивно.

k-reciprocal

k-reciprocal - множество элементов из top-k, в числе k ближайших которых присутствует сам запрос. На базе этого множества строят процесс переранжирования выдачи, один из которых описан в статье Re-ranking Person Re-identification with k-reciprocal Encoding. По определению, k-reciprocal ближе к запросу, чем k-nearest neighbors. Соответственно, можно грубо считать элементы, попавшие в множество k-reciprocal заведомо позитивными и изменять правило взвешивания, например, для Query Expansion. В данной статье разработан механизм пересчета дистанций с использований k-reciprocal множеств самих элементов в top-k. В статье много выкладок, это выходит за рамки данного поста, поэтому предлагаю читателю ознакомиться самостоятельно.

Валидация

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

Метрики

В первую очередь - метрики. Рассмотрим такие популярные метрики в задаче Image Retrieval: precision@k, recall@k, R-precision, mAP и nDCG.

precision@k

Показывает долю релевантных среди top-k ответов.

Плюсы:

  • показывает, насколько система избирательна в построении top-k

Минусы:

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

  • достичь значение 1 возможно только, если число релевантных >= k для всех запросов

R-precision

То же самое, что precision@k, где k устанавливается равным числу релевантных к данному запросу

Плюсы:

  • исчезает чувствительность к числу k в precision@k, метрика становится стабильной

Минусы:

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

recall@k

Показывает, какая доля релевантных была найдена в top-k

Плюсы:

  • отвечает на вопрос, найдены ли релевантные в принципе среди top-k

  • стабильна и хорошо усредняется по запросам

mAP (mean Average Precision)

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

Плюсы:

  • объективная стабильная оценка качества поиска

  • является одно-численным представлением precision-recall кривой, которая сама по себе богата информацией для анализа

Минусы

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

Подробнее про метрики в Information Retrieval, в том числе посмотреть вывод mAP, можно почитать здесь.

nDCG (Normalized Discounted Gain)

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

Усреднение

Также важно отметить варианты усреднения метрик по запросам. Рассмотрим два варианта:

macro: для каждого запроса считается метрика, усредняем по всем запросам

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

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

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

Схемы валидации

Предлагаю рассмотреть два варианта валидации.

Валидация на множестве запросов и выбранных к ним релевантных

На вход: изображения-запросы и изображения, релевантные к ним. Имеется разметка в виде списка релевантных для данного запроса.

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

Валидация на полной базе

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

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

Пример реализованного проекта

Некоторые компании спорят с другими компаниями, чтобы вторые не использовали изобразительные элементы бренда первых. В таких случаях более слабый производитель пытается паразитировать на успехе крупного бренда, выдавая свои продукты и услуги под похожей символикой. От этого страдают и пользователи - вы можете по ошибке купить сыр не того производителя, которому вы уже доверяете, а взять подделанный товар, не прочитав внимательно этикетку. Пример из недавнего: Apple Is Attempting to Block a Pear Logo Trademark.

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

Устройство нашей системы

Для обучения, валидации и разработки поискового приложения мы разработали такую систему. Здесь Training pipeline, Benchmark, Indexer и Demo Web app - независимые репозитории, Logo Search app - поисковое приложение одного из клиентов. Объем индексируемой базы изображений: 1.5 млн товарных знаков.

Примеры работы

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

Заключение

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

Подробнее..

Перевод От инвалида до киборга при помощи руки с ИИ

26.04.2021 18:04:48 | Автор: admin

Будущее здесь безо всяких преувеличений. В нашей публикации Третий глаз для незрячих рассказывалось о том, как можно облегчить жизнь незрячим людям при помощи нескольких ультразвуковых сенсоров. Сегодня рассказываем о кибернетической руке на основе глубокого обучения, точность вычислений которой составляет более 95 %. Также в статье есть впечатления смельчака, решившегося опробовать технологию на себе. Именно его вы видите на КДПВ.


Послушать историю [на английском] можно на SoundCloud

На этой неделе появилось 600 новых статей об архитектуре Transformer. Как мне быть? Случайным образом выбрать из них несколько, опубликовать у себя практически без изменений (не считая каких-то мелочей) и, возможно, чуть-чуть улучшить?

Надеюсь, вы не слишком обескуражены таким вступлением, но прошу понять меня правильно: архитектура Transformer сегодня настолько популярна, что сообщения о ней забивают все другие. Само собой, это потрясающая архитектура, она может оказаться чрезвычайно полезной во многих случаях, и недаром большинство исследователей сходят по ней с ума, но в области искусственного интеллекта (ИИ) есть и другие вещи, и, поверьте, не менее, а даже более увлекательные! Не стоит волноваться, я, естественно, буду рассказывать о впечатляющих проектах, созданных на базе архитектуры Transformer, применяемой в NLP, машинном распознавании образов и во множестве других областей. Я считаю эту архитектуру весьма перспективной, но просто пересказывать содержание новых работ, внося в них лишь косметические изменения, для меня не так интересно.

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

Связанная статья. Сможет ли архитектура Transformer заменить CNN в машинном распознавании образов?

Обратимся же теперь к настоящей теме этой статьи! Тема эта не имеет никакого отношения ни к архитектуре Transformer, ни даже к GAN, в ней нет никаких модных словечек (за исключением, пожалуй, слова "киберпанк"), но, тем не менее, это одно из самых крутых применений ИИ, с которыми я сталкивался в последнее время! Эта штука способна решать насущные проблемы многих людей и круто изменить их жизнь к лучшему. Конечно, она работает не так эффектно, как, например, превращение человеческого лица в персонажа аниме или мультфильма, зато ничто не сравнится с её полезностью.

Представляю вашему вниманию "Портативный автоматический ручной нейропротез с управлением пальцами на основе методов глубокого обучения", авторы: Nguyen, Drealan и др. Или, выражаясь словами одного из авторов, перед вами рука "киберпанка"!

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

Теперь давайте более подробно ознакомимся с этой уникальной и поражающей воображение новой работой.

В этой работе на нейропротез накладываются технологии глубокого обучения, позволяющие контролировать в реальном времени движения отдельных пальцев протеза. Человек, потерявший руку 14 лет назад, теперь может двигать искусственными пальцами, как на обычной руке! Задержка прохождения команд составляет от 50 до 120 миллисекунд, точность движений от 95 до 99 %. Из этой работы следует, что встраивание технологий глубоких нейросетей непосредственно в носимые биомедицинские устройства не только возможно, но и чрезвычайно эффективно!

Настоящий киборг!Настоящий киборг!

В данном случае был использован модуль NVIDIA Jetson Nano, специально разработанный для развёртывания систем ИИ в автономных приложениях. Это позволило использовать GPU и мощные библиотеки, такие как TensorFlow и PyTorch, внутри самого манипулятора. Авторы проекта говорят: "При реализации нашего нейронного декодера мы отыскали самый подходящий компромисс между размерами, мощностью и производительностью". Главная цель данной работы решить проблему эффективного развёртывания нейронных декодеров глубокого обучения на портативном устройстве, используемом в реальных приложениях, для долгосрочного применения в клинической практике.

НейропротезНейропротез

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

Схема NVIDIA Jet NanoСхема NVIDIA Jet Nano

На рисунке показан поток обработки данных платформой Jetson Nano. Сначала данные в виде сигналов периферических нервов от ампутированной руки отправляются на платформу. Эти данные предварительно обрабатываются. Этот шаг очень важен: берётся выборка входных необработанных нейронных данных, после чего система рассчитывает их основные характеристики во временной области, а затем загружает в модели. Такие предварительно обработанные данные соответствуют основным характеристикам нейронных данных односекундной давности, полученных от ампутированной руки и очищенных от источников шума. Затем эти прошедшие обработку данные передаются в модель глубокого обучения, и на выходе получается конечный результат возможность управления движением каждого пальца. Всего наборов выходных данных пять, по одному на каждый палец.

Как в реальности работает использованная авторами модель? В её основе лежит применение свёрточного слоя. Такой слой используется для идентификации различных представлений входных данных. В данном случае количество свёрток равняется 64. Эти свёртки были получены с использованием различных фильтров, то есть всего имеется 64 различных представления.

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

Блоки GRU сообщают модели, что делала рука в последнюю секунду (что было закодировано сначала) и что ей нужно делать дальше (что декодируется в настоящее время). Говоря простым языком, GRU это не что иное, как улучшенная версия рекуррентных нейронных сетей, или RNN.

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

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

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

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

Вот, что рассказывает сам пациент:

Я понимаю, что эта штука ещё требует доработки. В ней должно быть больше "жизненных" функций для выполнения повседневных задач, чтобы можно было не задумываться о том, в каком положении находится рука и в каком режиме она запрограммирована. Надо чтобы она работала так: увидел, дотянулся и взял. [...] В идеале я должен ощущать на теле не протез, а обычную руку. Я полагаю, мы до этого дойдём. Я верю в это!

Для меня данное изобретение самый невероятный пример применения технологий искусственного интеллекта.

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

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы Ссылки
  • [1] Nguyen & Drealan et al. (2021) A Portable, Self-Contained Neuroprosthetic Hand with Deep Learning-Based Finger Control.

  • [2]. Luu & Nguyen et al. (2021) Deep Learning-Based Approaches for Decoding Motor Intent from Peripheral Nerve Signals.

  • [3]. Nguyen et al. (2021) Redundant Crossfire: A Technique to Achieve Super-Resolution in Neurostimulator Design by Exploiting Transistor Mismatch: https://experts.umn.edu/en/publications/redundant-crossfire-a-technique-to-achieve-super-resolution-in-ne

  • [4]. Nguyen & Xu et al. (2020) A Bioelectric Neural Interface Towards Intuitive Prosthetic Control for Amputees

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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