التعلم الالي. الشبكات العصبية (الجزء 2): النمذجة OR ؛ XOR مع TensorFlow.js

المقالة هي استمرار لسلسلة من المقالات المكرسة للتعلم الآلي باستخدام مكتبة TensorFlow.JS ، تقدم المقالة السابقة الجزء النظري العام لتدريب أبسط شبكة عصبية تتكون من خلية عصبية واحدة:



التعلم الآلي. الشبكات العصبية (الجزء الأول): عملية التعلم الخاصة بالمدرك



في هذه المقالة ، سوف نستخدم شبكة عصبية لنمذجة تنفيذ العمليات المنطقية OR ؛ XOR ، وهو نوع من تطبيقات "Hello World" للشبكات العصبية.

ستصف المقالة خطوة بخطوة عملية مثل هذه النمذجة باستخدام TensorFlow.js.



لذلك دعونا نبني شبكة عصبية لعملية OR المنطقية. عند الإدخال ، سنرسل دائمًا إشارتين X 1 و X 2 ، وعند المخرجات سوف نتلقى إشارة خرج واحدة Y. ​​لتدريب الشبكة العصبية ، نحتاج أيضًا إلى مجموعة بيانات تدريب (الشكل 1).





الشكل 1 - مجموعة بيانات تدريبية ونموذج لنمذجة عملية OR منطقية



لفهم بنية الشبكة العصبية المراد تعيينها ، دعنا نتخيل مجموعة بيانات تدريب على مستوى إحداثيات باستخدام المحورين X 1 و X 2 (الشكل 2 ، يسار).





الشكل 2 - مجموعة التدريب على المستوى الإحداثي للتشغيل المنطقي أو



يرجى ملاحظة أنه لحل هذه المشكلة ، يكفي أن نرسم خطًا يقسم المستوى بحيث توجد جميع القيم TRUE على جانب واحد من الخط ، وعلى الجانب الآخر - جميع القيم FALSE (الشكل 2 ، صحيح). نعلم أيضًا أن خلية عصبية واحدة في الشبكة العصبية (perceptron) يمكنها التعامل تمامًا مع هذا الغرض ، حيث يتم حساب قيمة الإخراج من إشارات الإدخال على النحو التالي:



ذ=x1ث1+x2ث2

وهو تمثيل رياضي لمعادلة الخط المستقيم.



في ضوء حقيقة أن قيمنا في النطاق من 0 إلى 1 ، فإننا نطبق أيضًا وظيفة التنشيط السيني. وهكذا ، تبدو شبكتنا العصبية كما في الشكل 3.



الشكل 3 - الشبكة العصبية لتدريب العمليات المنطقية أو ،



لذلك دعونا نحل هذه المشكلة باستخدام TensorFlow.js.



أولاً ، نحتاج إلى تحويل مجموعة بيانات التدريب إلى موترات. الموتر هو حاوية بيانات يمكن أن تحتوي علىنمحاور N وعدد عشوائي من العناصر على طول كل محور. معظم الموترات على دراية بالرياضيات - المتجهات (موتر بمحور واحد) ، المصفوفات (موتر بمحورين - صفوف ، أعمدة).



لتحديد مجموعة بيانات التدريب ، يكون المحور الأول (المحور 0) دائمًا هو المحور الذي توجد على طوله جميع مثيلات عينات البيانات المتاحة (الشكل 4).





الشكل 4 - هيكل الموتر



في حالتنا المحددة ، لدينا 4 حالات لعينات البيانات (الشكل 1) ، مما يعني أن موتر الإدخال على طول المحور الأول سيكون به 4 عناصر. كل عنصر من عناصر عينة التدريب عبارة عن ناقل يتكون من عنصرين X1، X2... وبالتالي ، فإن موتر الإدخال يحتوي على محورين (مصفوفة) ، على طول المحور الأول هناك 4 عناصر ، على طول المحور الثاني - عنصران.



const input = [[0, 0], [1, 0], [0, 1], [1, 1]];
const inputTensor = tf.tensor(input, [input.length, 2]);


وبالمثل ، قم بتحويل الإخراج إلى موتر. بالنسبة لإشارات الإدخال ، على طول المحور الأول لدينا 4 عناصر ، ويحتوي كل عنصر على متجه يحتوي على قيمة واحدة:



const output = [[0], [1], [1], [1]]
const outputTensor = tf.tensor(output, [output.length, 1]);


لنقم بإنشاء نموذج باستخدام TensorFlow API:



const model = tf.sequential();
model.add(
      tf.layers.dense({ inputShape: [2], units: 1, activation: 'sigmoid' })
);


سيبدأ إنشاء النموذج دائمًا باستدعاء tf.sequential () . اللبنة الأساسية للنموذج هي الطبقات. يمكننا الاتصال بالنموذج بعدد طبقات الشبكة العصبية الذي نحتاجه. هنا نستخدم طبقة كثيفة ، مما يعني أن كل خلية عصبية في الطبقة التالية لها اتصال مع كل خلية عصبية في الطبقة السابقة. على سبيل المثال ، إذا كان لدينا طبقتان كثيفتان ، في الطبقة الأولىن الخلايا العصبية ، وفي الثانية -م ، سيكون العدد الإجمالي للوصلات بين الطبقاتنم .

في حالتنا ، كما نرى ، تتكون الشبكة العصبية من طبقة واحدة ، يوجد فيها خلية عصبية واحدة ، وبالتالي يتم ضبط الوحدات على واحدة.



أيضًا ، بالنسبة للطبقة الأولى من الشبكة العصبية ، يجب علينا تعيين شكل الإدخال ، حيث يتم تمثيل كل مثيل إدخال بواسطة متجه من قيمتين X1و X2، وبالتالي شكل الإدخال = [2] . لاحظ أنهليست هناك حاجةلتعيين شكل الإدخال للطبقات المتوسطة - يمكن لـ TensorFlow تحديد هذه القيمة من قيمة الوحدات الخاصةبالطبقة السابقة.

أيضًا ، إذا لزم الأمر ، يمكن تعيين وظيفة تنشيط لكل طبقة ، وقد قررنا أعلاه أن هذه ستكون وظيفة سينية. يمكن العثور على وظائف التنشيط المتاحة حاليًا في TensorFlow هنا .



بعد ذلك ، نحتاج إلى تجميع النموذج (انظر API هنا ) ، بينما نحتاج إلى تعيين معلمتين مطلوبتين - هذه هي وظيفة الخطأ ونوع المحسن الذي سيبحث عن الحد الأدنى له:



model.compile({
    optimizer: tf.train.sgd(0.1),
    loss: 'meanSquaredError'
});


لقد قمنا بتعيين هبوط التدرج العشوائي على أنه المُحسِّن بخطوة تدريب 0.1.



قائمة المحسّنين المنفذين في المكتبة: tf.train.sgd ، tf.train.momentum ، tf.train.adagrad ، tf.train.adadelta ، tf.train.adam ، tf.train.adamax ، tf.train.rmsprop .  

من الناحية العملية ، بشكل افتراضي ، يمكنك تحديد مُحسِّن adam على الفور ، والذي يحتوي على أفضل معدلات تقارب النماذج ، على عكس sgd - يتم تعيين معدل التعلم في كل مرحلة من مراحل التدريب اعتمادًا على تاريخ الخطوات السابقة وليس ثابتًا طوال عملية التعلم بأكملها.


كدالة خطأ ، يتم تقديمها من خلال دالة الخطأ التربيعي لمتوسط ​​الجذر:

إل=1نأنا=1ن(ذصصهدأناجرهد(أنا)-ذهxصهجرهد(أنا))2



تم تعيين النموذج ، والخطوة التالية هي عملية تدريب النموذج ، لذلك يجب استدعاء طريقة الملاءمة على النموذج :



async function initModel() {
    // skip for brevity

    await model.fit(trainingInputTensor, trainingOutputTensor, {
        epochs: 1000,
        shuffle: true,
        callbacks: {
            onEpochEnd: async (epoch, { loss }) => {
                // any actions on during any epoch of training
                await tf.nextFrame();
            }
        }
    })
}


لقد حددنا أن عملية التعلم يجب أن تتكون من 100 خطوة تعلم (عدد فترات التعلم) ؛ أيضًا في كل حقبة متتالية - يجب خلط بيانات الإدخال بترتيب عشوائي ( خلط عشوائي = صحيح ) - مما يؤدي إلى تسريع عملية تقارب النموذج ، حيث توجد حالات قليلة في مجموعة بيانات التدريب الخاصة بنا (4).



بعد الانتهاء من عملية التدريب ، يمكننا استخدام طريقة التنبؤ ، والتي ، بناءً على إشارات الإدخال الجديدة ، ستحسب قيمة الإخراج.



const testInput = generateInputs(10);
const testInputTensor = tf.tensor(testInput, [testInput.length, 2]);

const output = model.predict(testInputTensor).arraySync();


و generateInputs طريقة ببساطة يولد مجموعة بيانات 10X10 العينة التي تقسم تنسيق الطائرة إلى 100 الساحات:

[[0و0]و[0و0.1]و[0و0.2]و......[1و1]]





يتم إعطاء الرمز الكامل هنا
import React, { useEffect, useState } from 'react';
import LossPlot from './components/LossPlot';
import Canvas from './components/Canvas';
import * as tf from "@tensorflow/tfjs";

let model;

export default () => {
    const [data, changeData] = useState([]);
    const [lossHistory, changeLossHistory] = useState([]);

    useEffect(() => {
        async function initModel() {
            const input = [[0, 0], [1, 0], [0, 1], [1, 1]];
            const inputTensor = tf.tensor(input, [input.length, 2]);

            const output = [[0], [1], [1], [1]]
            const outputTensor = tf.tensor(output, [output.length, 1]);

            const testInput = generateInputs(10);
            const testInputTensor = tf.tensor(testInput, [testInput.length, 2]);

            model = tf.sequential();
            model.add(
            tf.layers.dense({ inputShape:[2], units:1, activation: 'sigmoid'})
            );
            model.compile({
                optimizer: tf.train.adam(0.1),
                loss: 'meanSquaredError'
            });

            await model.fit(inputTensor, outputTensor, {
                epochs: 100,
                shuffle: true,
                callbacks: {
                    onEpochEnd: async (epoch, { loss }) => {
                        changeLossHistory((prevHistory) => [...prevHistory, {
                            epoch,
                            loss
                        }]);

                        const output = model.predict(testInputTensor)
                                                       .arraySync();
                        changeData(() => output.map(([out], i) => ({
                            out,
                            x1: testInput[i][0],
                            x2: testInput[i][1]
                        })));
                        await tf.nextFrame();
                    }
                }
            })
        }
        initModel();
    }, []);

    return (
        <div>
            <Canvas data={data} squareAmount={10}/>
            <LossPlot loss={lossHistory}/>
        </div>
    );
}

function generateInputs(squareAmount) {
    const step = 1 / squareAmount;
    const input = [];
    for (let i = 0; i < 1; i += step) {
        for (let j = 0; j < 1; j += step) {
            input.push([i, j]);
        }
    }
    return input;
}




في الشكل التالي سترى جزءًا من عملية التعلم:







تنفيذ بلانكر:




محاكاة العملية المنطقية XOR



مجموعة التدريب لهذه الوظيفة موضحة في الشكل 6 ، وسنضع هذه النقاط أيضًا كما فعلنا للعملية المنطقية أو على مستوى الإحداثيات





الشكل 6 - مجموعة بيانات التدريب ونموذج لنمذجة العملية المنطقية حصرية أو (XOR)



يرجى ملاحظة ذلك على عكس العملية المنطقية OR ، لا يمكنك تقسيم المستوى بخط مستقيم واحد ، بحيث توجد في جانب واحد كل القيم TRUE ، وعلى الجانب الآخر - الكل FALSE . ومع ذلك ، يمكننا القيام بذلك باستخدام منحنيين (الشكل 7).



من الواضح ، في هذه الحالة ، أن خلية عصبية واحدة في الطبقة لا تكفي - فأنت بحاجة إلى طبقة أخرى على الأقل تحتوي على خليتين عصبيتين ، كل منهما سيحدد أحد الخطين على المستوى.





الشكل 7 - نموذج الشبكة العصبية للعملية المنطقية EXCLUSIVE OR (XOR)



في الكود السابق ، نحتاج إلى إجراء تغييرات في عدة أماكن ، أحدها هو مجموعة بيانات التدريب نفسها:



const input = [[0, 0], [1, 0], [0, 1], [1, 1]];
const inputTensor = tf.tensor(input, [input.length, 2]);

const output = [[0], [1], [1], [0]]
const outputTensor = tf.tensor(output, [output.length, 1]);


المكان الثاني هو الهيكل المتغير للنموذج ، حسب الشكل 7:



model = tf.sequential();
model.add(
    tf.layers.dense({ inputShape: [2], units: 2, activation: 'sigmoid' })
);
model.add(
    tf.layers.dense({ units: 1, activation: 'sigmoid' })
);


تبدو عملية التعلم في هذه الحالة كما يلي:







تنفيذ بلانكر:




موضوع المقال التالي



في المقالة التالية سوف نصف كيفية حل المشاكل المتعلقة بتصنيف الكائنات إلى فئات ، بناءً على قائمة ببعض الميزات.



All Articles