Създайте предсказател на цифрови емотикони в бележника на Jupyter, като предавате данни между JavaScript и Python. Обикновено JavaScript се използва за визуализация на данни в преносими компютри, но може да се използва и за прототипиране на преден край/UI за модели за дълбоко обучение.

Защо, по дяволите, да създаваме потребителски интерфейс в Notebook?

Кратък отговор:

Споделете бележника на модела на Deep Learning с колеги от различни екипи като Business, Data Science, Front End разработчик, DevOps за тяхното мнение, преди да започне истинската разработка на софтуер.

Ноутбукът Jupyter е предназначен за бързо създаване на прототипи и всеки знае това, но това, което много пропускат е, че можем да направим и бърз прототип на потребителския интерфейс. Много специалисти по данни бързо започват да изграждат големия модел на дълбоко обучение с малкото данни, с които разполагат, без дори да мислят какво разработват и за кого 🤷‍♂️. Повярвайте ми, първият модел, от който сте доволни (на базата на показатели като точност), няма да бъде внедрен в производство поради различни причини като бавно време за извод, липса на поддръжка на устройството, изчерпване на паметта бла бла.

Дори опитни мениджъри на проекти/продукти понякога няма да знаят какво наистина искат, така че първо се опитайте да получите споразумението за работния процес или конвейера от всички участващи играчи, дори и при това някои неща ще се промъкнат. „Аз съм учен по данни, моята работа е само да създавам ML модели не е правилният начин на мислене, поне според мен.“ Малко познания по бизнес, уеб разработка, DevOps могат да ви помогнат да спестите много време в опити за отстраняване на грешки или да обяснят защо създаденият от вас модел/работен процес е най-добрият за бизнеса. По-добре да си майстор на всички сделки тук. Между другото, това не означава, че трябва да поемете отговорностите на вашите колеги. Просто се опитайте да разберете какво се случва извън вашия кръг. В тази публикация ми позволете да споделя пример за това как създаването на прост потребителски интерфейс за вход и изход за модел на задълбочено обучение може да ви помогне да видите пукнатините в работния си процес и да увеличите производителността.

Приложение

Ако не сте запознати с набора от данни MNIST, просто знайте, че можете да изградите Digit Emoji Predictor, тъй като наборът от данни съдържа изображения в сивата скала на ръчно нарисувани цифри, от нула до девет. Така че нашият потребителски интерфейс ще позволи на потребителя да нарисува цифра и да покаже съответните емотикони.

Първо ще решим какво ще строим? Вместо да създаваме от нулата, нека се опитаме да пресъздадем Digit Recognizer в бележника, без да използваме Django. Случаят на използване, който избрахме, е страхотен, защото не можем просто да използваме pywidgets, това ни принуждава да копаем дълбоко в света на HTML и JavaScript. В по-дългосрочен план този маршрут има повече предимства поради неограничените възможности, безплатните ресурси и което е по-важно по-близо до действителния преден край. Разбира се, ако всичко, което искате, е плъзгач, тогава просто превъртете с pywidgets. Първо ще настроим задния край и след това ще се върнем към потребителския интерфейс.

Разпознаване на цифрови емотикони

Обучих модела за задълбочено обучение на CNN с помощта на pytorch, просто ще изтеглим модела и теглата от тук. Това е прост модел с 3 блока Conv и 2 напълно свързани слоя. Трансформациите, използвани за този модел, са Resize & ToTensor. При извикване на функцията predict на изображението ще получим предвидената цифра.

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms

# Digit Recognizer model definition
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=5)
        self.conv2 = nn.Conv2d(32, 32, kernel_size=5)
        self.conv3 = nn.Conv2d(32,64, kernel_size=5)
        self.fc1 = nn.Linear(3*3*64, 256)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.dropout(x, p=0.5, training=self.training)
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = F.dropout(x, p=0.5, training=self.training)
        x = F.relu(F.max_pool2d(self.conv3(x),2))
        x = F.dropout(x, p=0.5, training=self.training)
        x = x.view(-1,3*3*64 )
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return x



# Predict the digit given the tensor
def predict(image, modelName = 'mnist_model.pth'):

    # Resize before converting the image to Tensor
    Trfms = transforms.Compose([transforms.Resize(28), transforms.ToTensor()])

    # Apply transformations and increase dim for mini-batch dimension
    imageTensor = Trfms(image).unsqueeze(0)

    # Download model from URL and load it
    model = torch.load(modelName)

    # Activate Eval mode
    model.eval()

    # Pass the image tensor by the model & return max index of output
    with torch.no_grad():
        out = model(imageTensor)
        return int(torch.max(out, 1)[1])

Имаме нужда от помощна функция, за да преобразуваме BASE64 изображение (област на платното, в която потребителят ще начертае цифрата) в PIL изображение.

import re, base64
from PIL import Image
from io import BytesIO

# Decode the image drawn by the user
def decodeImage(codec):

    # remove the front part of codec
    base64_data = re.sub('^data:image/.+;base64,', '', codec)

    # base64 decode
    byte_data = base64.b64decode(base64_data)

    # Convert to bytes
    image_data = BytesIO(byte_data)

    # Convert to image & convert to grayscale
    img = Image.open(image_data).convert('L')
    return img

Сега нека комбинираме двете функции, а именно decodeImage и predict като отделна функция. Този начин е по-лесен, тъй като изходът от функцията (HTML код на emoji) може директно да бъде предаден на HTML тага чрез JavaScript. Изчакайте малко, ако сте объркани, нещата ще станат много по-лесни за проследяване в следващия раздел.

# Decode the image and predict the value
def decode_predict(imgStr):

    # Decode the image
    image = decodeImage(imgStr)

    # Declare html codes for 0-9 emoji
    emojis = [ 
            "0️⃣",
             "1️⃣",
             "2️⃣",
             "3️⃣",
             "4️⃣",
             "5️⃣",
             "6️⃣",
             "7️⃣",
             "8️⃣",
             "9️⃣"
    ]

    # Call the predict function
    digit = predict(image)

    # get corresponding emoji
    return emojis[digit]

UI

Частта за HTML и CSS е доста проста, JavaScript позволява на потребителя да рисува вътре в платното и да извиква функции чрез бутоните Predict и Clear. Бутонът Clear почиства платното и задава резултата на 🤔 емотикони. Ще настроим JS функцията на бутона Predict в следващия раздел.

from IPython.display import HTML

html = """
<div class="outer">
        <! -- HEADER SECTION -->
    <div> 
        <h3 style="margin-left: 30px;"> Digit Emoji Predictor &#128640; </h3> 
        <br>
        <h7 style="margin-left: 40px;"> Draw a digit from &#48;&#65039;&#8419; - &#57;&#65039;&#8419;</h7>
    </div>

    <div>
           <! -- CANVAS TO DRAW THE DIGIT -->
        <canvas id="canvas" width="250" height="250" style="border:2px solid; float: left; border-radius: 5px; cursor: crosshair;">
        </canvas>

            <! -- SHOW PREDICTED DIGIT EMOJI-->
        <div class="wrapper1"> <p id="result">&#129300;</p></div>

            <! -- BUTTONS TO CALL DL MODEL & CLEAR THE CANVAS -->
        <div class="wrapper2">
            <button type="button" id="predictButton" style="color: #4CAF50;margin:10px;">  Predict </button>  
            <button type="button" id="clearButton" style="color: #f44336;margin:10px;">  Clear </button>  
        </div>
    </div>
</div>
"""


css = """
<style>
    .wrapper1 {
      text-align: center;
      display: inline-block;
      position: absolute;
      top: 82%;
      left: 25%;
      justify-content: center;
      font-size: 30px;
    }
    .wrapper2 {
      text-align: center;
      display: inline-block;
      position: absolute;
      top: 90%;
      left: 19%;
      justify-content: center;
    }

    .outer {
        height: 400px; 
        width: 400px;
        justify-content: center;
    }

</style>
"""


javascript = """

<script type="text/javascript">
(function() {
    /* SETUP CANVAS & ALLOW USER TO DRAW */
    var canvas = document.querySelector("#canvas");
    canvas.width = 250;
    canvas.height = 250;
    var context = canvas.getContext("2d");
    var canvastop = canvas.offsetTop
    var lastx;
    var lasty;
    context.strokeStyle = "#000000";
    context.lineCap = 'round';
    context.lineJoin = 'round';
    context.lineWidth = 5;

    function dot(x, y) {
        context.beginPath();
        context.fillStyle = "#000000";
        context.arc(x, y, 1, 0, Math.PI * 2, true);
        context.fill();
        context.stroke();
        context.closePath();
    }

    function line(fromx, fromy, tox, toy) {
        context.beginPath();
        context.moveTo(fromx, fromy);
        context.lineTo(tox, toy);
        context.stroke();
        context.closePath();
    }
    canvas.ontouchstart = function(event) {
        event.preventDefault();
        lastx = event.touches[0].clientX;
        lasty = event.touches[0].clientY - canvastop;
        dot(lastx, lasty);
    }
    canvas.ontouchmove = function(event) {
        event.preventDefault();
        var newx = event.touches[0].clientX;
        var newy = event.touches[0].clientY - canvastop;
        line(lastx, lasty, newx, newy);
        lastx = newx;
        lasty = newy;
    }
    var Mouse = {
        x: 0,
        y: 0
    };
    var lastMouse = {
        x: 0,
        y: 0
    };
    context.fillStyle = "white";
    context.fillRect(0, 0, canvas.width, canvas.height);
    context.color = "black";
    context.lineWidth = 10;
    context.lineJoin = context.lineCap = 'round';
    debug();
    canvas.addEventListener("mousemove", function(e) {
        lastMouse.x = Mouse.x;
        lastMouse.y = Mouse.y;
        Mouse.x = e.pageX - canvas.getBoundingClientRect().left;
        Mouse.y = e.pageY - canvas.getBoundingClientRect().top;
    }, false);
    canvas.addEventListener("mousedown", function(e) {
        canvas.addEventListener("mousemove", onPaint, false);
    }, false);
    canvas.addEventListener("mouseup", function() {
        canvas.removeEventListener("mousemove", onPaint, false);
    }, false);
    var onPaint = function() {
        context.lineWidth = context.lineWidth;
        context.lineJoin = "round";
        context.lineCap = "round";
        context.strokeStyle = context.color;
        context.beginPath();
        context.moveTo(lastMouse.x, lastMouse.y);
        context.lineTo(Mouse.x, Mouse.y);
        context.closePath();
        context.stroke();
    };

    function debug() {
        /* CLEAR BUTTON */
        var clearButton = $("#clearButton");
        clearButton.on("click", function() {
            context.clearRect(0, 0, 250, 250);
            context.fillStyle = "white";
            context.fillRect(0, 0, canvas.width, canvas.height);

            /* Remove Result */
            document.getElementById("result").innerHTML = "&#129300;";
        });
        $("#colors").change(function() {
            var color = $("#colors").val();
            context.color = color;
        });
        $("#lineWidth").change(function() {
            context.lineWidth = $(this).val();
        });
    }
}());

</script>

"""


HTML(html + css + javascript)

Страхотен! Можем да рисуваме вътре в платното и при щракване върху бутона Clear, платното се почиства. Сложната част започва, когато потребителят щракне върху бутона Predict, тъй като трябва да направим следното:

  • JS към Python 👉 Вземете чертежа вътре в платното и го предайте на python
/* PASS CANVAS BASE64 IMAGE TO PYTHON VARIABLE imgStr*/
    var imgData = canvasObj.toDataURL();
    var imgVar = 'imgStr';
    var passImgCode = imgVar + " = '" + imgData + "'";
    var kernel = IPython.notebook.kernel;
    kernel.execute(passImgCode);
  • Python към JS 👉 Задайте предвидените емотикони като стойност на HTML елемент (#result)
/* CALL PYTHON FUNCTION "decode_predict" WITH "imgStr" AS ARGUMENT */
    function handle_output(response) {
        /* UPDATE THE HTML BASED ON THE OUTPUT */
        var result = response.content.data["text/plain"].slice(1, -1);
        document.getElementById("result").innerHTML = result;
    }
    var callbacks = {
        'iopub': {
            'output': handle_output,
        }
    };
    var getPredictionCode = "decode_predict(imgStr)";
    kernel.execute(getPredictionCode, callbacks, { silent: false });

Свързване на всички заедно.

predictJS = """
<script type="text/javascript">

/* PREDICTION BUTTON */
$("#predictButton").click(function() {
    var canvasObj = document.getElementById("canvas");
    var context = canvas.getContext("2d");

    /* PASS CANVAS BASE64 IMAGE TO PYTHON VARIABLE imgStr*/
    var imgData = canvasObj.toDataURL();
    var imgVar = 'imgStr';
    var passImgCode = imgVar + " = '" + imgData + "'";
    var kernel = IPython.notebook.kernel;
    kernel.execute(passImgCode);

    /* CALL PYTHON FUNCTION "decode_predict" WITH "imgStr" AS ARGUMENT */
    function handle_output(response) {
        /* UPDATE THE HTML BASED ON THE OUTPUT */
        var result = response.content.data["text/plain"].slice(1, -1);
        document.getElementById("result").innerHTML = result;
    }
    var callbacks = {
        'iopub': {
            'output': handle_output,
        }
    };
    var getPredictionCode = "decode_predict(imgStr)";
    kernel.execute(getPredictionCode, callbacks, { silent: false });
});
</script>

"""
HTML(predictJS)

Имаме както добри, така и лоши новини, хубавото е, че връзката за данни между Python и JavaScript работи, но всички прогнозни стойности са грешни (съпоставени с една и съща стойност — 8). Нека сравним изображението на платното, предадено на python и изображението на набор от данни MNIST.

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

import PIL.ImageOps

# Decode the image and predict the value
def decode_predict(imgStr):

    # Decode the image
    image = decodeImage(imgStr)

    # Invert the image as model expects black background & white strokes
    image = PIL.ImageOps.invert(image)

    # Declare html codes for 0-9 emoji
    emojis = [ 
            "&#48;&#65039;&#8419;",
             "&#49;&#65039;&#8419;",
             "&#50;&#65039;&#8419;",
             "&#51;&#65039;&#8419;",
             "&#52;&#65039;&#8419;",
             "&#53;&#65039;&#8419;",
             "&#54;&#65039;&#8419;",
             "&#55;&#65039;&#8419;",
             "&#56;&#65039;&#8419;",
             "&#57;&#65039;&#8419;"
    ]

    # Call the predict function
    digit = predict(image)

    # get corresponding emoji
    return emojis[digit]

Страхотно! Работи.

Заключение

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

Надяваме се, че успях да ви убедя, че основните умения за уеб разработчици са наистина полезни за специалист по данни. Бележникът може да бъде достъпен от тук. Чувствайте се свободни да се свържете чрез коментари или Twitter.

Първоначално публикувано в https://dev.to на 30 април 2021 г.