نویسندگان : محیا مهدیان و محمد حسن ستاریان
مقدمه
در هر مسئله دستهبندی (Classification)، پیادهسازی یک مدل مناسب -از جهت صحت پیشبینی- به مولفههای متفاوتی وابسته است؛ داشتن ورودیهای -با ویژگیهای- گوناگون و به تعداد زیاد یکی از مولفههای مهم برای آموزش مدلی که دقیق (Accurate) بوده و قادر به عمومیت بخشی (Generalization) به دانش خود و راستی آزمایی آن در محیط واقعی است میباشد. با این حال جمع آوری داده زیاد کاری سخت و نیازمند زمان و هزینه زیاد، بسته به شرایط و نوع داده مورد نیاز خواهد بود. در این شرایط و با توجه به اینکه کارهای پیشین زیادی انجام شده اند، استفاده از روشهای یادگیری انتقال (Transfer learning)، درواقع استفاده از یک مدل از پیش آموزش دیده و استفاده از آن به عنوان استخراج کننده ویژگی (Feature Extractor) یا به عنوان شبکه Fine Tuning کمک بسیار زیادی در ساخت مدلی دقیق و مناسب خواهد داشت. همچنین استفاده از روشهای افزایش داده (Data augmentation) باعث افزایش چشمگیر داده شده و در آموزش بهتر مدل موثر خواهد بود؛ با اینحال در استفاده از این روش باید توجه داشت از روشهایی برای تغییر عکس استفاده کرد که عکس خروجی خارج از فضای حالت مسئله نبوده و در شرایط واقعی مسئله وجود داشته باشد. به علاوه در جمعآوری داده باید دادههای جمع آوری شده بررسی و در صورت نیاز پاکسازیهایی نیز انجام شود، تا دادههای نامناسب، نامربوط و بیتاثیر حذف شوند اما از عمومیت دادهها کم نشده و دادههای مختلفی در شرایط مختلف آزمون مسئله وجود داشته باشد؛ چرا که هرچه دادهها متفاوتتر باشند مدل قابلیت عمومیت بخشی بیشتری خواهد داشت. استفاده از پارامترهای (Hyperparameter) مناسب در آموزش مدل، انتخاب دقیق و بسته به شرایط -و داده- پارامترها نیز از اهمیت زیادی برخوردار است و شاید نیاز باشد تغییر دقت مدل با تغییر این پارامترها بررسی شود که در اینصورت نیاز است مدل چندین دفعه آموزش داده شود.
مراحل پیادهسازی مدل
هدف ما آموزش مدلی بود تا بتواند مکانهای دانشگاه را تشخیص بدهد؛ برای پیادهسازی مدل خود از فریمورک کراس (Keras) و زبان پایتون استفاده کردیم. از آنجایی که تعداد کلاسها کم بوده و امکان جمعآوری داده زیادی که بتواند مدل را خوب آموزش بدهد نبود، مدلی برای استفاده به عنوان مدل پایه (Base Model) برای Fine Tuning انتخاب شد. سپس دادههای مورد نیاز جمعآوری شده، تمیز شده و آماده آموزش شدند. مدل روی گوگل کولب (Google Colab) به همراه استفاده از روشهای افزایش داده آموزش داده شده و ذخیره شد. برای استفاده از مدل برنامهای برای اجرای آن روی سرور و گرفتن خروجی با میکروفریمورک فلسک نوشته شده و اپلیکیشن اندرویدی نیز برای گرفتن عکس و پیشبینی آن در لحظه ساخته شد. هر مرحله به تفضیل توضیح داده خواهد شد:
کد تمامی مراحل در ریپوزتوری «SRU-Place-Recognizer» قابل دسترسی است.
پیدا کردن مدل پایه
همانطور که گفته شد از آنجایی که تعداد کلاسها کم بوده و امکان جمعآوری داده زیادی که بتواند مدل را خوب آموزش بدهد نبود، از روش یادگیری انتقال (Transfer learning) استفاده کرده و مدل VGG16 Places365 برای استفاده به عنوان مدل پایه (Base Model) برای Fine Tuning انتخاب شد. این مدل یک شبکه VGG16 است که از پیش با دادههای دیتاست Places365 که شامل بیش از ۱۰ میلیون عکس در بیش از ۴۰۰ موضوع است آموزش داده شده است؛ پس نه تنها ویژگیهای اولیه مورد نیاز مسئله در لایههای ابتدایی شناسایی شدهاند بلکه در لایههای جلوتر نیز ویژگیهای بصری عمیقی شناسایی شده اند و مدلی بسیار مناسب برای این مسئله خواهد بود. بنابراین ما از این مدل به عنوان مدل پایه آموزش خود استفاده کردیم به صورتی که تنها لایههای کانولوشنی استفاده شده و از میان آنها ۵ لایه آخر را نیز از حالت فریز (freeze) خارج کردیم. در قسمت ساخت مدل بیشتر بخوانید.
جمعآوری داده
برای مسئله شش کلاس -شش مکان برای آموزش مدل- در نظر گرفته شد: دانشکده کامپیوتر، دانشکده معماری، سلف، بوفه، ساختمان امور فرهنگی و زمین ورزشی؛ برای جمعآوری داده از این مکانها عکس و فیلم از تمامی زوایای ساختمانها گرفته شد. همچنین سعی شد در زمانهای متفاوتی عکسبرداری انجام شود تا تصاویر از تنوع قابل قبولی در نور محیط برخوردار باشند. فریمهای فیلمها بعدا با استفاده از اسکریپت پایتونی زیر جدا شد تا نهایتا در هر کلاس (از هر مکان) ۸۰۰ عکس شامل ۵۰۰ عکس برای آموزش و ۳۰۰ عکس برای تست و مجموعا ۴۸۰۰ عکس تولید شود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import argparse import cv2 parser = argparse.ArgumentParser() parser.add_argument("videoAddress", help="Address of the video") parser.add_argument("initialNumber", help="initialNumber to start naming frames, like frame_[10].jpg") args = parser.parse_args() videoAddress = args.videoAddress initialNumber = args.initialNumber or 0 vidcap = cv2.VideoCapture(videoAddress) success,image = vidcap.read() count = 0 success = True while success: cv2.imwrite("frame_%d.jpg" % (initialNumber+count), image) # save frame as JPEG file success,image = vidcap.read() count += 1 |
از این اسکریپت به صورت زیر استفاده میکنیم (با این فرض که کدهای بالا را در فایلی با نام frameExtractor.py ذخیره کرده اید)؛ پارامتر اول آدرس فایل ویدئو و پارامتر دوم عددی برای شروع نامگذاری تصاویر اسکریپت است برای مواقعی که فریمهای چندین فایل ویدئو را میخواهیم جدا کنیم:
1 |
python frameExtractor.py './video.mp4' 24 |
آمادهسازی دادهها
از آنجایی که روش جمعآوری داده ما، جدا کردن فریم از فیلمهای گرفته شده بود، تصاویر نامربوط، برای نمونه از محیط اطراف ساختمان و یا تصاویر تار شده نیز در میان عکسها وجود داشت. همچنین تصاویر با کیفیت ۲۱۶۰*۳۸۴۰ گرفته شده بودند و هر کدام تقریبا حجمی بیش از ۳ مگابایت داشتند که برای آموزش شبکه بسیار سنگین بوده و ویژگیهای (features) بسیار زیادی تولید میکردند که برای برنامه خود تا این حد نیاز به جزئیات نداشتیم؛ همچنین عکسها به صورت landscape جدا شده بودند و نیاز به چرخواندن (rotate) داشتند. برای همین، با استفاده از برنامه ImageMagick تصاویر را ۹۰ درجه چرخوانده و سپس همگی را به سایز ۱۹۲*۱۰۸ تبدیل کردیم تا مدل در حین سبک شدن از ویژگیهای کافی برای آموزش برخوردار باشد.
برای آشنایی با Image Magick و نحوه انجام کار پست «کار با تصاویر توسط ImageMagick» را بخوانید.
در نهایت تصاویر در فولدرهای مربوطه Train و تست و زیرفولدرهایی با اسامی کلاسها قرار داده شدند. این اسم فولدرها بعدا در آموزش مدل و استفاده از دیتا جنریتور (Data Generator) به عنوان اسامی کلاسهای مدل تعریف میشوند. ساختار فولدربندی دادهها به صورت زیر شد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
. ├───Test │ ├───Architect Campus │ ├───Buffet │ ├───Computer Campus │ ├───Culture house │ ├───Field │ └───Self └───Train ├───Architect Campus ├───Buffet ├───Computer Campus ├───Culture house ├───Field └───Self |
ساخت مدل
همانطور که قبلا اشاره شد برای مسئله از Fine Tuning استفاده شد. مدل نهایی تشکیل شده است از لایههای کانولوشنی شبکه VGG16 Places365 که به عنوان مدل پایه استفاده شده است و ۵ لایه آخر آن از حالت حالت فریز (freeze) خارج شده و به دو لایه تماما متصل (Fully connected) با ۲۵۶ نود و ۲ نود که به ترتیب از Activation function های Relu (برای شناسایی nonlinearities) و Softmax (برای کد کردن نتیجه در ۶ کلاس) استفاده میکنند متصل شدند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Model Used as Base model in Fine Tuning base_model = VGG16_Places365(include_top=False, weights='places', input_shape=(108, 192, 3)) # making 5 last layers *Unfreeze* for layer in base_model.layers[:12]: layer.trainable = True for layer in base_model.layers[12:]: layer.trainable = False # Creating our very own model based on VGG616:Places365 model with additional # fully-connected layers containing 256 and 6 nodes, having, respectively Relu and Softmax as activation # functions for detecting nonlinearities and coding result in 6 classes model = models.Sequential() model.add(base_model) model.add(layers.Flatten()) model.add(layers.Dense(256, activation='relu')) model.add(layers.Dense(6, activation='softmax')) |
آموزش مدل
برای آموزش سریعتر مدل و استفاده از GPU که امکان استفاده آن در سیستم خودمان فعلا وجود نداشت، از سرویس گوگل کولب (Google Colab) استفاده کردیم. برای همین منظور فایلهای لازم برای آموزش مدل به گوگل درایو منتقل شدند -فایلها آپلود شده و از طریق سرویس SavetoDrive به گوگل درایو منتقل شدند- سپس فایلها را در نوتبوکی که در گوگل کولب ساخته بودیم وارد کردیم تا مدل را آموزش دهیم.
آموزش نحوه انتقال فایل از گوگل کولب به گوگل درایو را در پست «اتصال مستقیم سرویس کولب (Google Colab) به درایو (Google Drive) از طریق فایل سیستم FUSE» بخوانید.
برای آموزش مدل پس از تعریف ساختار مدل (که در قسمت ساخت مدل توضیح داده شد)، چون که تعداد دادهها زیاد بود از دیتا جنریتور (Data Generator) هایی برای خواندن تصاویر از فولدرهای مربوطه استفاده شده و برای دادههای آموزش (Train) از روشهای افزایش داده (Data augmentation) استفاده شد. تصاویر در گروههای ۲۰ تایی به شبکه تغذیه (Feed) شده ( batch_size = 20 )، مقادیر steps_per_epoch و validation_steps با توجه به تعداد دادههای Train و Test و تعداد عکسهای هر گروه ( batch_size) محاسبه شده و با ۱۰ بار تکرار ( epochs = 10 ) شبکه آموزش دید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# data agumentation methods are used to achive better result; methods to augument images selected # according to real-life situations to simulate real cases. train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, shear_range=0.2, zoom_range=0.2, fill_mode='nearest' ) # Note that the validation data should not be augmented! test_datagen = ImageDataGenerator(rescale=1./255) # data generators are used to feed the network from **Train** and **Test** directories. train_dir = 'Train' validation_dir = 'Test' train_generator = train_datagen.flow_from_directory( # This is the target directory train_dir, # All images will be resized to 108x192 target_size=(108, 192), batch_size=20, # Since we use categorical_crossentropy loss, we need categorical labels class_mode='categorical') validation_generator = test_datagen.flow_from_directory( validation_dir, target_size=(108, 192), batch_size=20, class_mode='categorical') # Compiling the model with *categorical_crossentropy* loss 'cause our problem is categorical # using RMSProp optimizer and *accuracy* metrics; then fitting it with created generators # within 30 epochs. (this step would take some time!) model.compile(loss='categorical_crossentropy', optimizer=optimizers.RMSprop(lr=2e-5), metrics=['acc']) history = model.fit_generator( train_generator, steps_per_epoch=149, epochs=10, validation_data=validation_generator, validation_steps=90) |
بررسی مدل
برای بررسی مدل نمودارهای روند تغییر accuracy و loss در هر epoch چاپ شد تا از نبود over-fitting مطمئن شویم.
به علاوه دقت مدل با پیشبینی تصاویری از مکانهای آموزش دیده که مدل قبلا آن عکسها را در دیتاست آموزش یا تست خود نداشته بررسی شد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from keras.preprocessing import image from keras.models import load_model from places_utils import preprocess_input # labels ordered corresponding to recognized classes by model. # extracted from --> label_map = (train_generator.class_indices) labels = ['Architect Campus', 'Buffet', 'Computer Campus', 'Culture house', 'Field', 'Self'] # predicting # ImageAddress is address to image img = image.load_img(ImageAddress, target_size=(108, 192)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) # printing prediction prediction = model.predict(x) # y_classes = y_prob.argmax() # --> uncomment for printing it's most probable Class Number # y_true_labels = train_generator.classes # --> uncomment for printing each train image Class Number print (prediction) # Printing each class probability for i, p in enumerate(prediction[0]): print('%s Probability: \t %f' % (labels[i], p)) |
تصویر با استفاده از تابع load_img در سایز مورد استفاده مدل خوانده شده و سپس به آرایه تبدیل شده، آرایه تبدیل به آرایه تک بعدی شده و پیش پردازشی رو آن توسط تابع preprocess_input انجام شده است. این تابع در فایل places_utils که مدل پایه (VGG16 Places365) در اختیار گذاشته موجود است.
ذخیره مدل
در نهایت برای استفادههای آتی، مدل را ذخیره کردیم.
1 2 |
# Saving model for further use. model.save('SRU_Places_6.h5') |
کد آموزش مدل و نوتبوک استفاده شده برای آموزش مدل در گوگل کولب در ریپازیتوری در دسترس اند.
استفاده از مدل در عمل
اسکریپت پیشبینی
برای اینکه از مدل استفاده کنیم، برنامهای لازم داشتیم تا تصویر را دریافت کرده و نتیجه پیشبینی را برگرداند. برای این منظور اسکریپت زیر نوشته شد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import argparse import numpy as np from keras.applications.imagenet_utils import _obtain_input_shape from keras.preprocessing import image from keras.models import load_model from places_utils import \ preprocess_input # places_utiles file is provided by VGG16:places contributors. parser = argparse.ArgumentParser() parser.add_argument("ImageAddress", help="Address of the images to predict.") args = parser.parse_args() ImageAddress = args.ImageAddress model = load_model('../Model/SRU_Places_6.h5') # labels ordered corresponding to recognized classes by model. # extracted from --> label_map = (train_generator.class_indices) labels = ['Architect Campus', 'Buffet', 'Computer Campus', 'Culture house', 'Field', 'Self'] # predicting img = image.load_img(ImageAddress, target_size=(108, 192)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) # printing prediction prediction = model.predict(x) # y_classes = y_prob.argmax() # --> uncomment for printing it's most probable Class Number # y_true_labels = train_generator.classes # --> uncomment for printing each train image Class Number print (prediction) # Printing each class probability for i, p in enumerate(prediction[0]): print('%s Probability: \t %f' % (labels[i], p)) |
اسکریپت در ریپازیتوری در دسترس است.
سرور پیشبینی
از آنجایی که برنامه بالا باید حتما به همراه عکسی که قرار است پیشبینی شود در یک سیستم باشند و درواقع به صورت لوکال اجرا میشود، محدود بوده و نیازهای استفاده عملی از مدل را فراهم نمیکند. برای همین با استفاده از میکروفریک ورک فلسک برنامه سرور زیر نوشته شد تا بتوان با آپلود عکس، نتیجه پیشبینی را دریافت کرد.
برای آشنایی با فلسک و نحوه ایجاد یک برنامه سرور پست «آموزش مقدماتی فلسک (Flask)» را بخوانید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# Packages Imports import base64 import json from subprocess import call import numpy as np from flask import Flask, flash, redirect, request, send_from_directory, url_for from keras.models import load_model from keras.preprocessing import image from werkzeug.utils import secure_filename import cv2 from places_utils import preprocess_input # define server app and model app = Flask(__name__) model = None # End-point to upload an image and call predict class for it # it upload an image encoded to base64. image string is place in request headers with key of 'file' @app.route('/upload', methods=['GET', 'POST']) def upload(): ''' uploads image and call predict for it.''' if request.method == 'POST': # file = request.json['headers']['file'] file = request.headers.get('file') imgdata = base64.b64decode(file) filename = 'imageToPredict.jpg' with open('uploads/'+filename, 'wb') as f: f.write(imgdata) f = predict('uploads/imageToPredict.jpg') return f '''TODO: Use image without saving in disk''' # def data_uri_to_cv2_img(encoded_data): # # encoded_data = uri.split(',')[1] # imgdata = base64.b64decode(encoded_data) # # nparr = np.fromstring(imgdata, np.uint8) # nparr = np.asarray(imgdata, dtype=np.uint8) # img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # print(type(img)) # cv2.imshow('image', img) # # return img labels = ['Architect Campus', 'Buffet', 'Computer Campus', 'Culture house', 'Field', 'Self'] # End-point to predict last uploded image def predict(imgaddr): ''' predicts the last uploaded image an returns a string at last containing classes probability.''' global model img = cv2.imread(imgaddr) h, w, c = img.shape if w > h: # rotate image if it's in wrong orientation # rotation is done by ImageMagick so it sohuld be installed call(['mogrify', '-rotate', '90', 'uploads/imageToPredict.jpg']) img = image.load_img(imgaddr, target_size=(108, 192)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) if not model: print('------- loading model') model = load_model('PF-50-fixed 24-3-97.h5') features = model.predict(x) predicts = [] for i, p in enumerate(features[0]): item = '%s Probability: %f' % (labels[i], p) predicts.append(item) predicts_string = '\n'.join(predicts) return predicts_string # end-point to get the last image sent to predict @app.route('/imagetopredict') def uploaded_file(): # images sent overwrite each other so there is only one image to get return send_from_directory('uploads/', 'imageToPredict.jpg') # End-point to predict again last uploded image @app.route('/predictagain') def predict_again(): f = predict('uploads/imageToPredict.jpg') return f # RUN THE SERVER THING if __name__ == '__main__': app.config['SESSION_TYPE'] = 'filesystem' app.run(host='0.0.0.0', port=8080) |
سه endpoint برای کار با مدل تعریف شدند؛ upload/ برای آپلود عکس (عکس را به صورت base64 دریافت کرده و آن را ذخیره میکند)، imagetopredict/ دریافت آخرین عکسی که برای پیشبینی فرستاده شده و predictagain/ برای پیشبینی دوباره آخرین عکس آپلود شده. سپس، این برنامه روی سرور دپلوی شده و مدل آماده استفاده عملی شد.
کد برنامه سرور در ریپازیتوری در دسترس است.
برای آشنایی با نحوه دپلوی مدل پست «دپلوی کردن و استفاده از مدل در عمل (Model deployment)» را بخوانید (ما از روش سوم استفاده کردیم).
اپلیکیشن اندروید
حال که سروری داشتیم که با فرستادن عکس میتوانستیم نتیجه پیشبینی را دریافت کنیم، میخواستیم از هر جایی امکان فرستادن عکس را داشته باشیم؛ برای این منظور با استفاده از فریمورک Nativescript-Vue که ترکیب فریمورکهای Nativescript که برای ساخت اپلیکیشنهای اندروید و ios با استفاده از زبان جاوااسکریپت (javascript) است و Vue که یک فریمورک جاوااسکریپتی برای ساخت Progressive Web App هاست، اپلیکیشن اندرویدی برای پیشبینی تصاویر توسط مدل و با اتصال به سرور تولید شد.
شبکه های اجتماعی