كيفية إنشاء نظام بسيط للتعرف على الصور باستخدام TensorFlow (الجزء الأول)

هذه ليست مقدمة عامة للذكاء الاصطناعي أو التعلم الآلي أو التعلم العميق. يوجد بالفعل الكثير من المقالات الرائعة التي تغطي هذه الموضوعات (على سبيل المثال هنا أو هنا).

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

بدلاً من ذلك ، يعد هذا المنشور وصفًا تفصيليًا لكيفية البدء في التعلم الآلي من خلال بناء نظام قادر (إلى حد ما) على التعرف على ما يراه في الصورة.

أنا حاليًا في رحلة للتعرف على الذكاء الاصطناعي وتعلم الآلة. والطريقة التي أتعلم بها بشكل أفضل هي ليس فقط قراءة الأشياء ، ولكن في الواقع من خلال بناء الأشياء والحصول على بعض الخبرة العملية. وهذا ما يدور حوله هذا المنشور. أريد أن أوضح لك كيف يمكنك بناء نظام يؤدي مهمة بسيطة في رؤية الكمبيوتر: التعرف على محتوى الصورة.

أنا نفسي لا أدعي أنني خبير. ما زلت أتعلم ، وهناك الكثير لنتعلمه. أنا أصف ما كنت ألعب به ، وإذا كان مثيرًا للاهتمام أو مفيدًا لك إلى حد ما ، فهذا رائع! من ناحية أخرى ، إذا وجدت أخطاء أو لديك اقتراحات للتحسين ، فيرجى إبلاغي بذلك ، حتى أتمكن من التعلم منك.

لا تحتاج إلى أي خبرة سابقة في التعلم الآلي لتتمكن من المتابعة. تمت كتابة رمز المثال بلغة Python ، لذا فإن المعرفة الأساسية ب Python ستكون رائعة ، لكن معرفة أي لغة برمجة أخرى ربما تكون كافية.

لماذا التعرف على الصور؟

يعد التعرف على الصور مهمة رائعة لتطوير واختبار مناهج التعلم الآلي. لا جدال في أن الرؤية هي أقوى إحساس لدينا وتأتي بشكل طبيعي إلينا كبشر. لكن كيف نفعل ذلك بالفعل؟ كيف يترجم الدماغ الصورة الموجودة على شبكية العين إلى نموذج عقلي لمحيطنا؟ لا أعتقد أن أحدا يعرف بالضبط.

النقطة المهمة هي أنه يبدو أنه من السهل علينا القيام بذلك - سهل للغاية لدرجة أننا لا نحتاج حتى إلى بذل أي جهد واعي فيه - ولكن من الصعب على أجهزة الكمبيوتر القيام به (في الواقع ، قد لا يكون الأمر بهذه السهولة بالنسبة لنا أيضًا ، ربما نحن نحن لا ندرك حجم العمل الذي تقوم به. يبدو أن أكثر من نصف دماغنا يشارك بشكل مباشر أو غير مباشر في الرؤية).

كيف يمكننا جعل أجهزة الكمبيوتر تقوم بمهام مرئية ونحن لا نعرف حتى كيف نقوم بها بأنفسنا؟ وهنا يأتي دور التعلم الآلي. بدلاً من محاولة التوصل إلى إرشادات مفصلة خطوة بخطوة حول كيفية تفسير الصور وترجمتها إلى برنامج كمبيوتر ، فإننا نترك الكمبيوتر يكتشفها بنفسه.

الهدف من التعلم الآلي هو إعطاء أجهزة الكمبيوتر القدرة على القيام بشيء ما دون إخباره صراحة بكيفية القيام بذلك. نحن فقط نقدم نوعًا من البنية العامة ونمنح الكمبيوتر الفرصة للتعلم من التجربة ، على غرار الطريقة التي نتعلم بها نحن البشر من التجربة أيضًا.

ولكن قبل أن نبدأ في التفكير في حل كامل لرؤية الكمبيوتر ، دعنا نبسط المهمة إلى حد ما وننظر إلى مشكلة فرعية محددة يسهل علينا التعامل معها.

تصنيف الصور ومجموعة بيانات CIFAR-10

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

سوف نستخدم مجموعة بيانات موحدة تسمى CIFAR-10. يتكون CIFAR-10 من 60.000 صورة. هناك 10 فئات مختلفة و 6000 صورة لكل فئة. يبلغ حجم كل صورة 32 × 32 بكسل فقط. يجعل الحجم الصغير من الصعب أحيانًا علينا نحن البشر التعرف على الفئة الصحيحة ، ولكنه يبسط الأشياء لنموذج الكمبيوتر الخاص بنا ويقلل من الحمل الحسابي المطلوب لتحليل الصور.

الطريقة التي ندخل بها هذه الصور في نموذجنا هي تغذية النموذج بمجموعة كاملة من الأرقام. يتم وصف كل بكسل بثلاثة أرقام فاصلة عائمة تمثل قيم الأحمر والأخضر والأزرق لهذا البكسل. ينتج عن هذا 32 × 32 × 3 = 3072 قيمة لكل صورة.

بصرف النظر عن CIFAR-10 ، هناك الكثير من مجموعات بيانات الصور الأخرى المستخدمة بشكل شائع في مجتمع رؤية الكمبيوتر. يخدم استخدام مجموعات البيانات الموحدة غرضين. أولاً ، يتطلب إنشاء مجموعة البيانات هذه الكثير من العمل. تحتاج إلى العثور على الصور ومعالجتها لتناسب احتياجاتك وتسمية كل منهم على حدة. السبب الثاني هو أن استخدام نفس مجموعة البيانات يسمح لنا بمقارنة الأساليب المختلفة مع بعضها البعض بموضوعية.

بالإضافة إلى ذلك ، أدت مجموعات بيانات الصور الموحدة إلى إنشاء قوائم ومسابقات درجات عالية في رؤية الكمبيوتر. ربما تكون المنافسة الأكثر شهرة هي مسابقة Image-Net ، حيث توجد 1000 فئة مختلفة للكشف عنها. الفائز بجائزة 2012 كان خوارزمية طورها أليكس كريجفسكي وإيليا سوتسكيفر وجيفري هينتون من جامعة تورنتو (ورقة تقنية) والتي هيمنت على المنافسة وفازت بها بفارق كبير. كانت هذه هي المرة الأولى التي يستخدم فيها النهج الفائز شبكة عصبية تلافيفية ، كان لها تأثير كبير على مجتمع البحث. الشبكات العصبية التلافيفية هي شبكات عصبية اصطناعية تم تصميمها بشكل فضفاض على غرار القشرة البصرية الموجودة في الحيوانات. كانت هذه التقنية موجودة منذ فترة ، ولكن في ذلك الوقت لم ير معظم الناس بعد قدرتها على أن تكون مفيدة.تغير هذا بعد مسابقة Image-Net لعام 2012. فجأة كان هناك الكثير من الاهتمام بالشبكات العصبية والتعلم العميق (التعلم العميق هو مجرد المصطلح المستخدم لحل مشاكل التعلم الآلي مع الشبكات العصبية متعددة الطبقات). يلعب هذا الحدث دورًا كبيرًا في بدء طفرة التعلم العميق في العامين الماضيين.

التعلم الخاضع للإشراف

كيف يمكننا استخدام مجموعة بيانات الصور لجعل الكمبيوتر يتعلم من تلقاء نفسه؟ على الرغم من أن الكمبيوتر يقوم بجزء التعلم بنفسه ، فلا يزال يتعين علينا إخباره بما يجب تعلمه وكيفية القيام به. الطريقة التي نقوم بها هي تحديد عملية عامة لكيفية تقييم الكمبيوتر للصور.

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

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

بعد انتهاء التدريب ، لا تتغير قيم معلمات النموذج بعد الآن ويمكن استخدام النموذج لتصنيف الصور التي لم تكن جزءًا من مجموعة بيانات التدريب الخاصة به.

TensorFlow

TensorFlow هي مكتبة برمجيات مفتوحة المصدر للتعلم الآلي ، والتي أصدرتها Google في عام 2015 وسرعان ما أصبحت واحدة من أكثر مكتبات التعلم الآلي شيوعًا والتي يستخدمها الباحثون والممارسون في جميع أنحاء العالم. نستخدمها للقيام برفع الأحمال العددي لنموذج تصنيف الصور الخاص بنا.

بناء النموذج ، مصنف Softmax

الكود الكامل لهذا النموذج متاح على جيثب. من أجل استخدامه ، يجب أن يكون لديك ما يلي:

  • Python (تم اختبار الكود باستخدام Python 2.7 ، ولكن يجب أن يعمل Python 3.3+ أيضًا ، رابط لإرشادات التثبيت)
  • TensorFlow (رابط لتعليمات التثبيت)
  • مجموعة بيانات CIFAR-10: قم بتنزيل إصدار Python من مجموعة البيانات من //www.cs.toronto.edu/~kriz/cifar.html أو استخدم الرابط المباشر للأرشيف المضغوط. ضع cifar-10-batches-py/الدليل المستخرج في الدليل حيث تقوم بوضع الكود المصدري للبيثون ، بحيث يكون المسار إلى الصور /path-to-your-python-source-code-files/cifar-10-batches-py/.

حسنًا ، نحن الآن على استعداد للذهاب أخيرًا. لنلقِ نظرة على الملف الرئيسي لتجربتنا ، softmax.pyونحللها سطراً بسطر:

يجب أن تكون البيانات المستقبلية موجودة في جميع ملفات TensorFlow Python لضمان التوافق مع كل من Python 2 و 3 وفقًا لدليل نمط TensorFlow.

ثم نقوم باستيراد TensorFlow و numpy للحسابات العددية ووحدة الوقت. data_helpers.pyيحتوي على وظائف تساعد في تحميل مجموعة البيانات وإعدادها.

نبدأ مؤقتًا لقياس وقت التشغيل وتحديد بعض المعلمات. سأتحدث عنها لاحقًا عندما نستخدمها بالفعل. ثم نقوم بتحميل مجموعة بيانات CIFAR-10. نظرًا لأن قراءة البيانات ليست جزءًا من جوهر ما نقوم به ، فقد وضعت هذه الوظائف في data_helpers.pyملف منفصل ، والذي يقرأ فقط الملفات التي تحتوي على مجموعة البيانات ويضع البيانات في بنية بيانات يسهل التعامل معها. .

One thing is important to mention though. load_data() is splitting the 60000 images into two parts. The bigger part contains 50000 images. This training set is what we use for training our model. The other 10000 images are called test set. Our model never gets to see those until the training is finished. Only then, when the model’s parameters can’t be changed anymore, we use the test set as input to our model and measure the model’s performance on the test set.

This separation of training and testing data is very important. We wouldn’t know how well our model is able to make generalizations if it was exposed to the same dataset for training and for testing. In the worst case, imagine a model which exactly memorizes all the training data it sees. If we were to use the same data for testing it, the model would perform perfectly by just looking up the correct solution in its memory. But it would have no idea what to do with inputs which it hasn’t seen before.

This concept of a model learning the specific features of the training data and possibly neglecting the general features, which we would have preferred for it to learn is called overfitting. Overfitting and how to avoid it is a big issue in machine learning. More information about overfitting and why it is generally advisable to split the data into not only 2 but 3 different datasets can be found in this video (youtube mirror) (the video is part of Andrew Ng’s great free machine learning course on Coursera).

To get back to our code, load_data() returns a dictionary containing

  • images_train: the training dataset as an array of 50,000 by 3,072 (= 32 pixels x 32 pixels x 3 color channels) values.
  • labels_train: 50000 labels for the training set (each a number between 0 nad 9 representing which of the 10 classes the training image belongs to)
  • images_test: test set (10,000 by 3,072)
  • labels_test: 10,000 labels for the test set
  • classes: 10 text labels for translating the numerical class value into a word (0 for ‘plane’, 1 for ‘car’, etc.)

Now we can start building our model. The actual numerical computations are being handled by TensorFlow, which uses a fast and efficient C++ backend to do this. TensorFlow wants to avoid repeatedly switching between Python and C++ because that would slow down our calculations.

The common workflow is therefore to first define all the calculations we want to perform by building a so-called TensorFlow graph. During this stage no calculations are actually being performed, we are merely setting the stage. Only afterwards we run the calculations by providing input data and recording the results.

So let’s start defining our graph. We first describe the way our input data for the TensorFlow graph looks like by creating placeholders. These placeholders do not contain any actual data, they just specify the input data’s type and shape.

For our model, we’re first defining a placeholder for the image data, which consists of floating point values (tf.float32). The shape argument defines the input dimensions. We will provide multiple images at the same time (we will talk about those batches later), but we want to stay flexible about how many images we actually provide. The first dimension of shape is therefore None, which means the dimension can be of any length. The second dimension is 3,072, the number of floating point values per image.

The placeholder for the class label information contains integer values (tf.int64), one value in the range from 0 to 9 per image. Since we’re not specifying how many images we’ll input, the shape argument is [None].

weights and biases are the variables we want to optimize. But let’s talk about our model first.

Our input consists of 3,072 floating point numbers and the desired output is one of 10 different integer values. How do we get from 3,072 values to a single one? Let’s start at the back. Instead of a single integer value between 0 and 9, we could also look at 10 score values — one for each class — and then pick the class with the highest score. So our original question now turns into: How do we get from 3,072 values to 10?

The simple approach which we are taking is to look at each pixel individually. For each pixel (or more accurately each color channel for each pixel) and each possible class, we’re asking whether the pixel’s color increases or decreases the probability of that class.

Let’s say the first pixel is red. If images of cars often have a red first pixel, we want the score for car to increase. We achieve this by multiplying the pixel’s red color channel value with a positive number and adding that to the car-score. Accordingly, if horse images never or rarely have a red pixel at position 1, we want the horse-score to stay low or decrease. This means multiplying with a small or negative number and adding the result to the horse-score.

For each of the 10 classes we repeat this step for each pixel and sum up all 3,072 values to get a single overall score, a sum of our 3,072 pixel values weighted by the 3,072 parameter weights for that class. In the end we have 10 scores, one for each class. Then we just look at which score is the highest, and that’s our class label.

The notation for multiplying the pixel values with weight values and summing up the results can be drastically simplified by using matrix notation. Our image is represented by a 3,072-dimensional vector. If we multiply this vector with a 3,072 x 10 matrix of weights, the result is a 10-dimensional vector containing exactly the weighted sums we are interested in.

The actual values in the 3,072 x 10 matrix are our model parameters. If they are random/garbage our output will be random/garbage. That’s where the training data comes into play. By looking at the training data we want the model to figure out the parameter values by itself.

All we’re telling TensorFlow in the two lines of code shown above is that there is a 3,072 x 10 matrix of weight parameters, which are all set to 0 in the beginning. In addition, we’re defining a second parameter, a 10-dimensional vector containing the bias. The bias does not directly interact with the image data and is added to the weighted sums. The bias can be seen as a kind of starting point for our scores.

Think of an image which is totally black. All its pixel values would be 0, therefore all class scores would be 0 too, no matter how the weights matrix looks like. Having biases allows us to start with non-zero class scores.

This is where the prediction takes place. We’ve arranged the dimensions of our vectors and matrices in such a way that we can evaluate multiple images in a single step. The result of this operation is a 10-dimensional vector for each input image.

The process of arriving at good values for the weights and bias parameters is called training and works as follows: First, we input training data and let the model make a prediction using its current parameter values. This prediction is then compared to the correct class labels. The numerical result of this comparison is called loss. The smaller the loss value, the closer the predicted labels are to the correct labels and vice versa.

We want to model to minimize the loss, so that its predictions are close to the true labels. But before we look at the loss minimization, let’s take a look at how the loss is calculated.

The scores calculated in the previous step, stored in the logits variable, contains arbitrary real numbers. We can transform these values into probabilities (real values between 0 and 1 which sum to 1) by applying the softmax function, which basically squeezes its input into an output with the desired attributes. The relative order of its inputs stays the same, so the class with the highest score stays the class with the highest probability. The softmax function’s output probability distribution is then compared to the true probability distribution, which has a probability of 1 for the correct class and 0 for all other classes.

We use a measure called cross-entropy to compare the two distributions (a more technical explanation can be found here). The smaller the cross-entropy, the smaller the difference between the predicted probability distribution and the correct probability distribution. This value represents the loss in our model.

Luckily TensorFlow handles all the details for us by providing a function that does exactly what we want. We compare logits, the model’s predictions, with labels_placeholder, the correct class labels. The output of sparse_softmax_cross_entropy_with_logits() is the loss value for each input image. We then calculate the average loss value over the input images.

But how can we change our parameter values to minimize the loss? This is where TensorFlow works its magic. Via a technique called auto-differentiation it can calculate the gradient of the loss with respect to the parameter values. This means that it knows each parameter’s influence on the overall loss and whether decreasing or increasing it by a small amount would reduce the loss. It then adjusts all parameter values accordingly, which should improve the model’s accuracy. After this parameter adjustment step the process restarts and the next group of images are fed to the model.

TensorFlow knows different optimization techniques to translate the gradient information into actual parameter updates. Here we use a simple option called gradient descent which only looks at the model’s current state when determining the parameter updates and does not take past parameter values into account.

Gradient descent only needs a single parameter, the learning rate, which is a scaling factor for the size of the parameter updates. The bigger the learning rate, the more the parameter values change after each step. If the learning rate is too big, the parameters might overshoot their correct values and the model might not converge. If it is too small, the model learns very slowly and takes too long to arrive at good parameter values.

The process of categorizing input images, comparing the predicted results to the true results, calculating the loss and adjusting the parameter values is repeated many times. For bigger, more complex models the computational costs can quickly escalate, but for our simple model we need neither a lot of patience nor specialized hardware to see results.

These two lines measure the model’s accuracy. argmax of logits along dimension 1 returns the indices of the class with the highest score, which are the predicted class labels. The labels are then compared to the correct class labels by tf.equal(), which returns a vector of boolean values. The booleans are cast into float values (each being either 0 or 1), whose average is the fraction of correctly predicted images.

We’re finally done defining the TensorFlow graph and are ready to start running it. The graph is launched in a session which we can access via the sess variable. The first thing we do after launching the session is initializing the variables we created earlier. In the variable definitions we specified initial values, which are now being assigned to the variables.

Then we start the iterative training process which is to be repeated max_steps times.

These lines randomly pick a certain number of images from the training data. The resulting chunks of images and labels from the training data are called batches. The batch size (number of images in a single batch) tells us how frequent the parameter update step is performed. We first average the loss over all images in a batch, and then update the parameters via gradient descent.

If instead of stopping after a batch, we first classified all images in the training set, we would be able to calculate the true average loss and the true gradient instead of the estimations when working with batches. But it would take a lot more calculations for each parameter update step. At the other extreme, we could set the batch size to 1 and perform a parameter update after every single image. This would result in more frequent updates, but the updates would be a lot more erratic and would quite often not be headed in the right direction.

Usually an approach somewhere in the middle between those two extremes delivers the fastest improvement of results. For bigger models memory considerations are very relevant too. It’s often best to pick a batch size that is as big as possible, while still being able to fit all variables and intermediate results into memory.

Here the first line of code picks batch_size random indices between 0 and the size of the training set. Then the batches are built by picking the images and labels at these indices.

Every 100 iterations we check the model’s current accuracy on the training data batch. To do this, we just need to call the accuracy-operation we defined earlier.

This is the most important line in the training loop. We tell the model to perform a single training step. We don’t need to restate what the model needs to do in order to be able to make a parameter update. All the info has been provided in the definition of the TensorFlow graph already. TensorFlow knows that the gradient descent update depends on knowing the loss, which depends on the logits which depend on weights, biases and the actual input batch.

We therefore only need to feed the batch of training data to the model. This is done by providing a feed dictionary in which the batch of training data is assigned to the placeholders we defined earlier.

After the training is completed, we evaluate the model on the test set. This is the first time the model ever sees the test set, so the images in the test set are completely new to the model. We’re evaluating how well the trained model can handle unknown data.

The final lines print out how long it took to train and run the model.

Results

Let’s run the model with with the command “python softmax.py”. Here is how my output looks like:

Step 0: training accuracy 0.14 Step 100: training accuracy 0.32 Step 200: training accuracy 0.3 Step 300: training accuracy 0.23 Step 400: training accuracy 0.26 Step 500: training accuracy 0.31 Step 600: training accuracy 0.44 Step 700: training accuracy 0.33 Step 800: training accuracy 0.23 Step 900: training accuracy 0.31 Test accuracy 0.3066 Total time: 12.42s

What does this mean? The accuracy of evaluating the trained model on the test set is about 31%. If you run the code yourself, your result will probably be around 25–30%. So our model is able to pick the correct label for an image it has never seen before around 25–30% of the time. That’s not bad!

There are 10 different labels, so random guessing would result in an accuracy of 10%. Our very simple method is already way better than guessing randomly. If you think that 25% still sounds pretty low, don’t forget that the model is still pretty dumb. It has no notion of actual image features like lines or even shapes. It looks strictly at the color of each pixel individually, completely independent from other pixels. An image shifted by a single pixel would represent a completely different input to this model. Considering this, 25% doesn’t look too shabby anymore.

What would happen if we trained for more iterations? That would probably not improve the model’s accuracy. If you look at results, you can see that the training accuracy is not steadily increasing, but instead fluctuating between 0.23 and 0.44. It seems to be the case that we have reached this model’s limit and seeing more training data would not help. This model is not able to deliver better results. In fact, instead of training for 1000 iterations, we would have gotten a similar accuracy after significantly fewer iterations.

One last thing you probably noticed: the test accuracy is quite a lot lower than the training accuracy. If this gap is quite big, this is often a sign of overfitting. The model is then more finely tuned to the training data it has seen, and it is not able to generalize as well to previously unseen data.

لقد تبين أن هذا المنشور طويل جدًا بالفعل. أود أن أشكرك على قراءة كل شيء (أو لتخطيها مباشرة إلى الأسفل)! أتمنى أن تكون قد وجدت شيئًا يثير اهتمامك ، سواء كان ذلك حول كيفية عمل مصنف التعلم الآلي أو كيفية إنشاء وتشغيل رسم بياني بسيط باستخدام TensorFlow. بالطبع ، لا يزال هناك الكثير من المواد التي أود إضافتها. حتى الآن ، تحدثنا فقط عن مصنف softmax ، والذي لا يستخدم حتى أي شبكات عصبية.

يغير منشور مدونتي التالي ما يلي: اكتشف إلى أي مدى يمكن أن يؤدي استخدام نموذج شبكة عصبية صغير إلى تحسين النتائج! اقرأها هنا.

شكرا للقراءة. يمكنك أيضًا التحقق من المقالات الأخرى التي كتبتها على مدونتي.