تنظیم Hyperparameter شبکه عصبی با Keras Tuner و تمرین‌های فصل ۱۰ | آموزش یادگیری ماشین

تنظیم Hyperparameter شبکه عصبی با Keras Tuner و تمرین‌های فصل ۱۰

توسط admin | گروه هوش مصنوعی | 1405/06/01

نظرات 0

تنظیم Hyperparameter شبکه عصبی با Keras Tuner و تمرین‌های فصل ۱۰ | آموزش یادگیری ماشین

تنظیم Hyperparameter شبکه عصبی با Keras Tuner و تمرین‌های فصل ۱۰

تا اینجا در این فصل دیدیم شبکه‌های عصبی از کجا آمده‌اند، یک MLP چیست و چگونه می‌توان از آن برای طبقه‌بندی و رگرسیون استفاده کرد. همچنین با Sequential API در Keras برای ساخت MLPها و با Functional API و Subclassing API برای ساخت معماری‌های پیچیده‌تر، از جمله مدل‌های Wide & Deep و مدل‌های چندورودی و چندخروجی، آشنا شدیم. ذخیره و بازیابی مدل، callbackها برای checkpoint و توقف زودهنگام، و TensorBoard برای مصورسازی را هم بررسی کردیم. اکنون می‌توان با شبکه‌های عصبی بسیاری از مسائل را حل کرد؛ اما هنوز یک پرسش مهم باقی مانده است: تعداد لایه‌های پنهان، تعداد نورون‌ها و سایر ابرپارامترها را چگونه انتخاب کنیم؟

تنظیم دقیق ابرپارامترهای شبکه عصبی

انعطاف‌پذیری شبکه‌های عصبی یکی از نقاط قوت و در عین حال یکی از ضعف‌های اصلی آن‌هاست، زیرا ابرپارامترهای زیادی برای تنظیم وجود دارد. حتی در یک MLP ساده می‌توان تعداد لایه‌ها، تعداد نورون‌ها، تابع فعال‌سازی هر لایه، روش مقداردهی اولیه وزن‌ها، نوع بهینه‌ساز، نرخ یادگیری، اندازه batch و موارد دیگر را تغییر داد. بنابراین یافتن ترکیب مناسب ابرپارامترها خود یک مسئله مهم است.

یک راه این است که مدل Keras را با wrapperهای KerasRegressor یا KerasClassifier از کتابخانه SciKeras به یک estimator سازگار با Scikit-Learn تبدیل کنیم و سپس مانند فصل ۲ از GridSearchCV یا RandomizedSearchCV استفاده کنیم. بااین‌حال، برای مدل‌های Keras راه مناسب‌تری وجود دارد: کتابخانه Keras Tuner. این کتابخانه چند راهبرد جست‌وجو ارائه می‌کند، بسیار قابل سفارشی‌سازی است و با TensorBoard به‌خوبی یکپارچه می‌شود.

در Colab ابتدا می‌توان Keras Tuner را نصب کرد و سپس آن را معمولاً با نام kt وارد کرد. تابع سازنده مدل باید یک شیء kt.HyperParameters بگیرد، محدوده مقادیر ابرپارامترها را تعریف کند و بر اساس مقادیر انتخاب‌شده مدل را بسازد و compile کند:

import keras_tuner as kt

def build_model(hp):
    n_hidden = hp.Int("n_hidden", min_value=0, max_value=8, default=2)
    n_neurons = hp.Int("n_neurons", min_value=16, max_value=256)
    learning_rate = hp.Float("learning_rate", min_value=1e-4, max_value=1e-2,
                             sampling="log")
    optimizer = hp.Choice("optimizer", values=["sgd", "adam"])
    if optimizer == "sgd":
        optimizer = tf.keras.optimizers.SGD(learning_rate=learning_rate)
    else:
        optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)

    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten())
    for _ in range(n_hidden):
        model.add(tf.keras.layers.Dense(n_neurons, activation="relu"))
    model.add(tf.keras.layers.Dense(10, activation="softmax"))
    model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,
                  metrics=["accuracy"])
    return model

فراخوانی hp.Int("n_hidden", min_value=0, max_value=8, default=2) بررسی می‌کند آیا ابرپارامتری با نام n_hidden قبلاً در شیء hp ثبت شده است یا نه. اگر ثبت شده باشد مقدار همان را برمی‌گرداند؛ در غیر این صورت یک ابرپارامتر صحیح بین ۰ تا ۸ ثبت می‌کند و مقدار پیش‌فرض ۲ را برمی‌گرداند. n_neurons مشابه آن است. برای learning_rate بازه از 1e-4 تا 1e-2 تعریف شده و sampling="log" باعث می‌شود مقیاس‌های مختلف نرخ یادگیری به‌طور متوازن نمونه‌برداری شوند. ابرپارامتر optimizer هم بین SGD و Adam انتخاب می‌کند.

بخش دوم تابع مدل Sequential را می‌سازد: ابتدا Flatten، سپس تعداد خواسته‌شده لایه پنهان Dense با ReLU، و در پایان یک لایه خروجی ۱۰ نورونی با softmax. مدل سپس compile و برگردانده می‌شود.

جست‌وجوی تصادفی با Keras Tuner

random_search_tuner = kt.RandomSearch(
    build_model, objective="val_accuracy", max_trials=5, overwrite=True,
    directory="my_fashion_mnist", project_name="my_rnd_search", seed=42)
random_search_tuner.search(X_train, y_train, epochs=10,
                           validation_data=(X_valid, y_valid))

RandomSearch ابتدا یک بار build_model() را با یک شیء HyperParameters خالی صدا می‌زند تا مشخصات تمام ابرپارامترها را جمع‌آوری کند. سپس در این مثال پنج trial اجرا می‌شود؛ در هر trial، مقادیر به‌صورت تصادفی از بازه‌های تعریف‌شده انتخاب می‌شوند، مدل ۱۰ epoch آموزش می‌بیند و در زیرشاخه‌ای از my_fashion_mnist/my_rnd_search ذخیره می‌شود. با overwrite=True پوشه پروژه از ابتدا ساخته می‌شود. اگر بار بعد overwrite=False و مثلاً max_trials=10 باشد، Tuner کار را از همان نقطه ادامه می‌دهد و پنج trial دیگر اجرا می‌کند.

چون هدف val_accuracy است، مدل‌هایی با دقت اعتبارسنجی بیشتر ترجیح داده می‌شوند. بهترین مدل‌ها و ابرپارامترها را می‌توان چنین گرفت:

top3_models = random_search_tuner.get_best_models(num_models=3)
best_model = top3_models[0]

top3_params = random_search_tuner.get_best_hyperparameters(num_trials=3)
print(top3_params[0].values)
{'n_hidden': 5,
 'n_neurons': 70,
 'learning_rate': 0.00041268008323824807,
 'optimizer': 'adam'}

هر Tuner توسط یک oracle هدایت می‌شود. پیش از هر trial، Tuner از oracle می‌پرسد trial بعدی چه باشد. در RandomSearch، oracle فقط ترکیب بعدی را تصادفی انتخاب می‌کند. چون oracle سوابق همه trialها را نگه می‌دارد، می‌توان بهترین trial را مستقیماً مشاهده کرد:

best_trial = random_search_tuner.oracle.get_best_trials(num_trials=1)[0]
best_trial.summary()
best_trial.metrics.get_last_value("val_accuracy")

اگر از بهترین مدل راضی باشیم، می‌توان آن را چند epoch دیگر روی مجموعه آموزش کامل آموزش داد، سپس روی test set ارزیابی و در نهایت مستقر کرد:

best_model.fit(X_train_full, y_train_full, epochs=10)
test_loss, test_accuracy = best_model.evaluate(X_test, y_test)

تنظیم پیش‌پردازش و آرگومان‌های fit با HyperModel

گاهی لازم است علاوه بر معماری مدل، ابرپارامترهای پیش‌پردازش یا آرگومان‌های model.fit()، مانند اندازه batch، نیز تنظیم شوند. در این حالت به‌جای یک تابع ساده، از kt.HyperModel ارث‌بری می‌کنیم و متدهای build() و fit() را پیاده‌سازی می‌کنیم. متد fit() می‌تواند بر اساس ابرپارامترها داده را پیش‌پردازش کند یا اندازه batch را تغییر دهد. در مثال زیر یک ابرپارامتر Boolean به نام normalize تعیین می‌کند آیا داده‌ها پیش از آموزش استاندارد شوند یا خیر:

class MyClassificationHyperModel(kt.HyperModel):
    def build(self, hp):
        return build_model(hp)

    def fit(self, hp, model, X, y, **kwargs):
        if hp.Boolean("normalize"):
            norm_layer = tf.keras.layers.Normalization()
            X = norm_layer(X)
        return model.fit(X, y, **kwargs)

Hyperband

hyperband_tuner = kt.Hyperband(
    MyClassificationHyperModel(), objective="val_accuracy", seed=42,
    max_epochs=10, factor=3, hyperband_iterations=2,
    overwrite=True, directory="my_fashion_mnist", project_name="hyperband")

Hyperband شبیه ایده successive halving است: ابتدا مدل‌های زیادی را برای تعداد کمی epoch آموزش می‌دهد، سپس ضعیف‌ترین‌ها را حذف می‌کند و فقط بهترین 1 / factor مدل‌ها را نگه می‌دارد. این فرایند تا ماندن یک مدل ادامه می‌یابد. max_epochs بیشترین تعداد epoch برای بهترین مدل را کنترل می‌کند و در این مثال کل فرایند دو بار تکرار می‌شود. برای هر تکرار Hyperband، تعداد کل epochهای اجراشده روی همه مدل‌ها تقریباً برابر max_epochs * (log(max_epochs) / log(factor)) ** 2 است؛ در این مثال حدود ۴۴ epoch.

می‌توان TensorBoard و EarlyStopping را همزمان به جست‌وجو وصل کرد:

root_logdir = Path(hyperband_tuner.project_dir) / "tensorboard"
tensorboard_cb = tf.keras.callbacks.TensorBoard(root_logdir)
early_stopping_cb = tf.keras.callbacks.EarlyStopping(patience=2)
hyperband_tuner.search(X_train, y_train, epochs=10,
                       validation_data=(X_valid, y_valid),
                       callbacks=[early_stopping_cb, tensorboard_cb])

در TensorBoard و بخش HPARAMS می‌توان تمام ترکیب‌های امتحان‌شده، معیارهای حاصل، نمای جدولی، مختصات موازی و ماتریس نمودار پراکندگی را دید. انتخاب trialهای با دقت اعتبارسنجی بالا کمک می‌کند اثر هر ابرپارامتر و تعامل بین ابرپارامترها بهتر درک شود.

بهینه‌سازی بیزی و روش‌های پیشرفته‌تر

Hyperband منابع را هوشمندتر از جست‌وجوی کاملاً تصادفی تخصیص می‌دهد، اما همچنان در هسته خود فضای ابرپارامترها را به‌صورت تصادفی کاوش می‌کند. Keras Tuner یک Tuner دیگر به نام kt.BayesianOptimization هم دارد که با برازش یک مدل احتمالاتی از نوع Gaussian process به‌تدریج نواحی امیدبخش فضای جست‌وجو را یاد می‌گیرد. پارامتر alpha میزان نویز مورد انتظار در سنجه‌های عملکرد trialها را نشان می‌دهد و مقدار پیش‌فرض آن 1e-4 است؛ beta میزان تمایل الگوریتم به اکتشاف نسبت به بهره‌برداری از نواحی خوب شناخته‌شده را کنترل می‌کند و پیش‌فرض آن ۲٫۶ است:

bayesian_opt_tuner = kt.BayesianOptimization(
    MyClassificationHyperModel(), objective="val_accuracy", seed=42,
    max_trials=10, alpha=1e-4, beta=2.6,
    overwrite=True, directory="my_fashion_mnist", project_name="bayesian_opt")
bayesian_opt_tuner.search([...])

تنظیم ابرپارامتر هنوز یک حوزه فعال پژوهشی است. الگوریتم‌های تکاملی برای بهینه‌سازی همزمان جمعیتی از مدل‌ها و ابرپارامترهایشان، جست‌وجوی معماری شبکه و سامانه‌های AutoML نیز به‌کار رفته‌اند. حتی روش‌هایی مانند Deep Neuroevolution، تکامل را به‌جای گرادیان کاهشی برای آموزش شبکه به کار می‌گیرند. با وجود این ابزارها، دانستن محدوده‌های معقول ابرپارامترها همچنان برای ساخت نمونه اولیه و محدودکردن فضای جست‌وجو بسیار مفید است.

تعداد لایه‌های پنهان

برای بسیاری از مسائل، یک لایه پنهان نتیجه‌ای قابل قبول می‌دهد. از نظر نظری یک MLP با یک لایه پنهان و تعداد کافی نورون می‌تواند توابع بسیار پیچیده را مدل کند، اما برای مسائل دشوار، شبکه عمیق از نظر تعداد پارامتر بسیار کارآمدتر از شبکه کم‌عمق است و می‌تواند همان ساختار پیچیده را با تعداد نورون بسیار کمتر مدل کند.

دلیل آن ساختار سلسله‌مراتبی بسیاری از داده‌های واقعی است. مانند رسم جنگل: اگر copy/paste ممنوع باشد باید هر برگ و شاخه و درخت را جداگانه رسم کرد؛ اما اگر بتوان یک برگ را تکثیر کرد و از آن شاخه، سپس درخت و در نهایت جنگل ساخت، کار بسیار سریع‌تر می‌شود. شبکه‌های عمیق نیز همین ویژگی را دارند: لایه‌های پایین ساختارهای ساده مانند قطعه‌خط‌ها را مدل می‌کنند؛ لایه‌های میانی آن‌ها را به ساختارهای پیچیده‌تر مانند مربع و دایره ترکیب می‌کنند؛ و لایه‌های بالاتر از آن‌ها مفاهیم سطح‌بالا مانند چهره می‌سازند.

این معماری سلسله‌مراتبی علاوه بر همگرایی سریع‌تر، تعمیم به مسئله جدید را هم آسان می‌کند. مثلاً اگر شبکه‌ای برای تشخیص چهره آموزش دیده باشد، برای مسئله تشخیص مدل مو می‌توان وزن‌های لایه‌های پایین آن را دوباره استفاده کرد تا شبکه لازم نباشد ویژگی‌های عمومی تصویر را از صفر بیاموزد. این همان یادگیری انتقالی (Transfer Learning) است.

در عمل برای بسیاری از مسائل یک یا دو لایه پنهان کافی است. در MNIST حتی یک لایه پنهان با چندصد نورون می‌تواند به بیش از ۹۷٪ دقت برسد و دو لایه با مجموع تقریباً همان تعداد نورون می‌تواند از ۹۸٪ عبور کند. برای مسائل پیچیده‌تر می‌توان تعداد لایه‌ها را تا نقطه‌ای افزایش داد که overfitting آغاز شود. وظایفی مانند طبقه‌بندی بزرگ‌مقیاس تصویر یا تشخیص گفتار ممکن است به ده‌ها یا صدها لایه نیاز داشته باشند؛ معمولاً چنین شبکه‌هایی از صفر آموزش داده نمی‌شوند و بخش‌هایی از یک شبکه pretrained پیشرفته دوباره استفاده می‌شود.

تعداد نورون‌ها در هر لایه پنهان

تعداد نورون‌های ورودی و خروجی را ماهیت مسئله تعیین می‌کند؛ مثلاً MNIST دارای 28×28=784 ورودی و ۱۰ نورون خروجی است. در گذشته معمول بود لایه‌های پنهان به شکل هرم طراحی شوند، مثلاً ۳۰۰، سپس ۲۰۰ و سپس ۱۰۰ نورون. این روش تا حد زیادی کنار گذاشته شده، زیرا در بسیاری از موارد استفاده از تعداد یکسان نورون در همه لایه‌های پنهان به همان اندازه یا بهتر کار می‌کند و تنها یک ابرپارامتر برای تنظیم باقی می‌گذارد. البته گاهی بزرگ‌تر بودن لایه پنهان اول مفید است.

می‌توان تعداد نورون‌ها را تدریجی افزایش داد تا شبکه شروع به overfit کند. راه جایگزین، ساخت مدلی کمی بزرگ‌تر از نیاز و کنترل overfitting با early stopping و regularization است. Vincent Vanhoucke این روش را «شلوار کشی» نامیده است: به‌جای صرف وقت برای یافتن اندازه کاملاً دقیق، مدلی بزرگ‌تر بسازید و اجازه دهید regularization آن را به اندازه مؤثر مناسب برساند. این رویکرد از ایجاد لایه‌های گلوگاهی جلوگیری می‌کند؛ اگر یک لایه بیش از حد کوچک باشد، بخشی از اطلاعات مفید ورودی را از دست می‌دهد و لایه‌های بعدی هرچقدر هم قوی باشند قادر به بازیابی آن اطلاعات نیستند.

نرخ یادگیری، اندازه batch و سایر ابرپارامترها

نرخ یادگیری

نرخ یادگیری احتمالاً مهم‌ترین ابرپارامتر است. معمولاً مقدار بهینه تقریباً نصف بیشترین نرخ یادگیری‌ای است که هنوز باعث واگرایی آموزش نمی‌شود. یک روش عملی برای یافتن آن این است که مدل را چندصد iteration آموزش دهیم و نرخ یادگیری را از مقدار بسیار کوچک، مثلاً 1e-5، به‌صورت نمایی تا مقدار بسیار بزرگ، مثلاً ۱۰، افزایش دهیم. سپس loss را برحسب نرخ یادگیری در مقیاس لگاریتمی رسم کنیم. ابتدا loss کاهش می‌یابد، اما در نقطه‌ای نرخ بیش از حد بزرگ می‌شود و loss جهش می‌کند. نرخ خوب معمولاً کمی پایین‌تر از این نقطه و اغلب حدود ۱۰ برابر کوچک‌تر از نقطه شروع صعود loss است. سپس مدل را از نو مقداردهی می‌کنیم و با همان نرخ مناسب آموزش معمول را انجام می‌دهیم.

بهینه‌ساز

انتخاب بهینه‌سازی بهتر از mini-batch gradient descent ساده و تنظیم ابرپارامترهای آن نیز اهمیت زیادی دارد. فصل ۱۱ چند بهینه‌ساز پیشرفته را بررسی می‌کند.

اندازه batch

اندازه batch هم بر عملکرد و هم بر زمان آموزش اثر قابل توجهی دارد. batchهای بزرگ روی شتاب‌دهنده‌هایی مانند GPU کارآمد هستند و نمونه‌های بیشتری در ثانیه پردازش می‌کنند؛ ازاین‌رو برخی توصیه می‌کنند بزرگ‌ترین batch قابل جاگیری در حافظه GPU استفاده شود. اما batch بزرگ، به‌ویژه اوایل آموزش، می‌تواند ناپایداری ایجاد کند و گاهی تعمیم بدتری از batch کوچک بدهد. پژوهش‌هایی batchهای کوچک ۲ تا ۳۲ را ترجیح داده‌اند، در حالی که پژوهش‌های دیگر نشان داده‌اند با تکنیک‌هایی مانند گرم‌کردن نرخ یادگیری می‌توان batchهای بسیار بزرگ تا ۸۱۹۲ را نیز با زمان آموزش کوتاه و بدون شکاف تعمیم به‌کار برد. یک راهبرد عملی این است که ابتدا batch بزرگ همراه با warmup نرخ یادگیری امتحان شود و اگر آموزش ناپایدار یا عملکرد نهایی ضعیف بود، batch کوچک‌تر استفاده شود.

تابع فعال‌سازی و تعداد iterationها

برای لایه‌های پنهان، ReLU معمولاً یک پیش‌فرض مناسب است؛ تابع فعال‌سازی لایه خروجی به نوع مسئله بستگی دارد. تعداد iterationهای آموزش نیز در بیشتر موارد لازم نیست به‌طور مستقیم تنظیم شود؛ early stopping معمولاً راه مناسب‌تری است.

با این بخش، مقدمه شبکه‌های عصبی مصنوعی و پیاده‌سازی آن‌ها با Keras به پایان می‌رسد. در فصل‌های بعد تکنیک‌های آموزش شبکه‌های بسیار عمیق، سفارشی‌سازی مدل‌ها با API سطح پایین TensorFlow، بارگذاری و پیش‌پردازش داده با tf.data و معماری‌هایی مانند CNN، RNN، Transformer، autoencoder و GAN بررسی خواهند شد.

تمرین‌ها

  1. TensorFlow Playground: چند طبقه‌بند دودویی بسازید و معماری و ابرپارامترها را تغییر دهید. موارد زیر را بررسی کنید:
    1. الگوهایی که شبکه می‌آموزد: شبکه پیش‌فرض را اجرا کنید و ببینید لایه اول الگوهای ساده و لایه دوم ترکیب‌های پیچیده‌تر را یاد می‌گیرد.
    2. توابع فعال‌سازی: tanh را با ReLU جایگزین کنید و تفاوت سرعت یادگیری و مرزهای خطی حاصل از شکل ReLU را مشاهده کنید.
    3. خطر کمینه‌های محلی: شبکه را به یک لایه پنهان با سه نورون کاهش دهید و چند بار با وزن‌های تازه آموزش دهید؛ زمان آموزش می‌تواند بسیار متغیر باشد و گاهی شبکه در یک کمینه محلی گیر می‌کند.
    4. شبکه بیش از حد کوچک: تعداد نورون‌ها را به دو کاهش دهید؛ شبکه پارامتر کافی ندارد و معمولاً underfit می‌کند.
    5. شبکه به‌اندازه کافی بزرگ: تعداد نورون‌ها را به هشت برسانید؛ آموزش معمولاً سریع و پایدار می‌شود. شبکه‌های بزرگ به‌ندرت در کمینه محلی بد گیر می‌کنند، هرچند می‌توانند مدت زیادی روی plateau بمانند.
    6. گرادیان محوشونده در شبکه عمیق: dataset مارپیچی را انتخاب کنید و چهار لایه پنهان با هشت نورون در هر لایه بسازید. کندی آموزش، plateauها و یادگیری سریع‌تر لایه‌های بالاتر را مشاهده کنید. مقداردهی اولیه بهتر، optimizerهایی مانند AdaGrad یا Adam و Batch Normalization می‌توانند این مشکل را کاهش دهند.
    7. بیشتر آزمایش کنید و با تغییر سایر پارامترها شهود خود را درباره رفتار شبکه‌های عصبی تقویت کنید.
  2. با نورون‌های مصنوعی کلاسیک مانند شکل ۱۰-۳، شبکه‌ای رسم کنید که A ⊕ B را محاسبه کند. راهنما: A ⊕ B = (A ∧ ¬B) ∨ (¬A ∧ B).
  3. چرا معمولاً طبقه‌بند Logistic Regression بر Perceptron کلاسیک ترجیح داده می‌شود؟ چگونه می‌توان Perceptron را طوری تغییر داد که معادل Logistic Regression شود؟
  4. چرا تابع sigmoid عنصر کلیدی در آموزش نخستین MLPها بود؟
  5. سه تابع فعال‌سازی مشهور را نام ببرید و نمودار آن‌ها را رسم کنید.
  6. فرض کنید MLP شامل ۱۰ نورون ورودی passthrough، یک لایه پنهان با ۵۰ نورون و یک لایه خروجی با ۳ نورون است و همه نورون‌ها ReLU دارند. شکل ماتریس ورودی X، ماتریس وزن و بردار bias لایه پنهان، ماتریس وزن و bias لایه خروجی و ماتریس خروجی Y را تعیین کنید و معادله Y را برحسب این کمیت‌ها بنویسید.
  7. برای طبقه‌بندی email به spam/ham چند نورون خروجی و چه تابع فعال‌سازی لازم است؟ برای MNIST چطور؟ برای پیش‌بینی قیمت مسکن مانند فصل ۲ چطور؟
  8. Backpropagation چیست و چگونه کار می‌کند؟ تفاوت آن با reverse-mode autodiff چیست؟
  9. تمام ابرپارامترهای قابل تنظیم در یک MLP پایه را فهرست کنید. اگر مدل روی داده آموزش overfit کند، برای کاهش آن کدام ابرپارامترها را چگونه تغییر می‌دهید؟
  10. یک MLP عمیق روی MNIST آموزش دهید و با تنظیم دستی ابرپارامترها به بیش از ۹۸٪ دقت برسید. نرخ یادگیری مناسب را با افزایش نمایی نرخ، رسم loss و یافتن نقطه جهش loss پیدا کنید. سپس همین کار را با Keras Tuner، checkpoint، early stopping و TensorBoard انجام دهید.

راه‌حل تمرین‌ها در notebook فصل کتاب ارائه شده است.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620