خانه / اخبار / چطوری کپچای سیستم گلستان رو با کمک یادگیری ماشین بشکنیم؟

چطوری کپچای سیستم گلستان رو با کمک یادگیری ماشین بشکنیم؟

اصل این مقاله به قلم جناب هادی عبدی خجسته در سایت Dataio.ir منتشر شده است و سایت مهندسی داده با هدف جمع آوری مطالب مفید حوزه علم داده به معرفی و بازنشر آن پرداخته است.

همه‌ی ما از کپچا (CAPTCHA) فراری ایم – همون تصاویر مزاحمی رو میگم که نوشته های درهم ریخته است و برای اینکه به فرم یا صفحه ای از سایت دسترسی داشته باشیم باید اونو تایپ کنیم. کپچاها برای تشخیص انسان از بات ها (همون برنامه های اتوماتیک) و معمولا برای جلوگیری از ورود به بخشی خاص، جلوگیری از پر کردن فرم ها یا کاهش برخی حملات طراحی شدند. جالبه بدونید در حال حاضر مدتی است که کپچاهای متنی محبوبیت خودشون رو از دست دادند و جای خودشون رو به کپچاهای تصویری یا موارد مشابه دادند.

نمونه ای از کپچای گوگل جایگزین مناسبی برای کپچاهای متنی
نمونه ای از کپچای گوگل جایگزین مناسبی برای کپچاهای متنی

سیستم گلستان رو تقریبا همه می شناسند. سیستم اتوماسیون معروف دانشگاه های ایران که پیدا کردن هر چیزی داخل اون نیازمند یک دوره دکتری تخصصی “زبانهای رسمی و روش های صوری” است!
توی این پست قصد داریم با رویکرد آموزشی و مرحله به مرحله، شکستن کپچای متنی سیستم گلستان رو با بهره گیری از قدرت یادگیری ماشین/عمیق و داده های آماری بررسی کنیم. توصیه میشه اگر دنبال دردسر نیستید این کار رو توی خونه امتحان نکنید😉 .


برای شروع ماجراجویی، اول نگاهی به فرم ورود سیستم گلستان به همراه تصویر کپچای متنی می اندازیم:

صفحه ورود سیستم گلستان
صفحه ورود سیستم گلستان

توی این صفحه یک تصویر جداگانه وجود داره که از طریق آدرس تصویر میشه بهش دسترسی داشت. بنابراین قدم اول ذخیره تعداد زیادی از این تصاویر هست تا با اون ها در مرحله بعد یک شبکه عصبی مصنوعی کانولوشنال (شبکه ای برای استخراج ویژگی ها از تصاویر و کلاس بندی) رو آموزش بدیم. نهایتا شبکه آموزش داده شده میتونه تصاویر ورودی رو به متن مورد نظر ترجمه کنه.

جمع آوری داده آموزشی

خُب، معمولا هر الگوریتم یادگیری ماشین برای آموزش نیازمند داده های زیادی هست. شکستن کپچا هم از این قاعده مستثنی نیست. بنابراین برای اینکه بتونیم یک کپچای متنی رو بشکنیم، نیازمند تعداد زیادی تصویر هستیم. برای اینکار یک اسکریپت ساده Shell میتونه چاره کار باشه.

1
2
3
4
5
6
#! /bin/bash
# use this script for downloading Golestan CAPTCHA
for ((i=;i < 10000000;i++)){
    wget -x --no-check-certificate https://support.nowpardaz.ir/frm/captcha/captcha.ashx -O ./$i.gif
}
exit

این اسکریپت سعی میکنه تعداد زیادی از فایل های تصویر رو دانلود کنه و با فرمت gif. ذخیره کنه (فرمت فایل های خروجی کدی هست که کپچا رو در سیستم اصلی تولید میکنه).

نمونه ای از تصاویر کپچاهای استخراج شده از سیستم گلستان
نمونه ای از تصاویر کپچاهای استخراج شده از سیستم گلستان

حالا حدالامکان تصاویر ورودی به سیستم رو به ساده ترین شکل ممکن تبدیل می کنیم. بدین منظور سعی میکنیم تا نوشته پایین تصاویر رو حذف کنیم.
در این مطلب از کتابخونه OpenCV فریم ورک محبوب بینایی کامپیوتر و پردازش تصویر و زبان ++C برای پیش پردازش تصاویر استفاده خواهیم کرد (اگر علاقمندید این کتابخونه Python API داره و می تونید از اون هم استفاده کنید).

1
2
3
4
5
Mat3b img = imread(argv[1]);    //Load image
   Rect roi(, , img.cols, img.rows / 1.11);    //Setup a rectangle to define
region of interest    Mat3b crop = img(roi);    //Crop the full image to
rectangle ROI    imshow("Original", img);
   imshow("Crop", crop);

کد بالا بخش پایین تصویر ورودی رو حذف میکنه که به عنوان یک آرگومان دریافت شده. بنابراین تا اینجای کار داده های آموزشی برای سیستم یادگیری مون رو در اختیار داریم.

پیش پردازش تصاویر

سیستم تشخیص کپچای ما می تونه مطابق شکل از شبکه عصبی مصنوعی برای پردازش تصاویر و برای شناسایی متن تصویر تشکیل شده باشه که یک تصویر از کپچای متنی رو به عنوان ورودی دریافت کرده و جواب صحیح رو تولید میکنه.

رویکرد کلی سیستم یادگیری برای شکستن کپچا
رویکرد کلی سیستم یادگیری برای شکستن کپچا

با داشتن داده های آموزشی کافی، رویکرد کلی بالا پاسخ درستی تولید می کنه؛ یعنی فقط کافیه تعداد خیلی زیادی تصویر رو به یکی از لایه های شبکه عصبی کانولوشنال (Convolutional Neural Network) (احتمالا با یک معماری پیچیده و با تعداد لایه و پارامتر زیاد) که وظیفه پردازش تصویر رو داره بدیم و با یک لایه دیگه از شبکه عصبی که وظیفه پیش بینی در مورد کل تصویر رو داره مسئله رو حل کنیم. اما این رویکرد با کمی تغییرات پاسخ های بهتر با محاسبات کمتر رو تولید میکنه. پس سعی میکنیم به جای پیش بینی کل تصویر فقط یک حرف رو پیش بینی کنیم و تصویر رو به چند تصویر شامل حروف (یا اعداد) تبدیل کنیم. واضح هست که برای تعداد زیادی تصویر راه حل فتوشاپ توصیه نمیشه!

خوشبختانه کپچایی که ما قصد شکستن اون رو داریم از پنج حرف تشکیل میشه. بنابراین اگر بتونیم روشی برای جداکردن حروف در تصویر داشته باشیم، شبکه عصبی تنها نیاز داره تا یک حرف رو کلاس بندی (Classify) کنه. یک روش ساده شکستن تصویر افقی به پنج قسمت مساوی هست. اما اگر به نمونه های دریافت شده در تصویر دقت کنید، ممکنه به دلیل جمع شدن حروف در یک طرف، حروف به درستی قطعه بندی (Segmentation) نشن. بنابراین برای اینکه محل دقیق تقسیم بندی در تصویر رو پیدا کنیم، سعی میکنیم تا تجمع پیکسل های غیر سفید در تصویر رو پیدا کنیم (یا به صورت ساده تعداد پیکسل در هر خط عمودی رو بشماریم)، و از اونجاییکه بین حروف فاصله کمی وجود داره، تعداد پیکسل بیشتر با تقریب خوبی جای هر حرف در تصویر رو به ما نشون میده. بعد از اینکار یک الگوریتم خوشه بندی (Clustering) می تونه به طور موثر تصویر رو به پنج بخش مختلف تقسیم کنه.

برای مطالعه :   مجموعه داده های کگل : یک سرویس حرفه ای برای میزبانی داده

ابتدا باید تصویر ورودی تبدیل به تصویر سیاه و سفید بشه و بخشی از نویز اون نیز حذف بشه تا مطمئن بشیم همه چیز به بهترین نحو ممکن انجام میشه.

1
2
3
4
5
6
7
8
   //Crop image and convert to gray, blur, sharpen, bitwise_not and black-white image
    Mat img_gray, img_sharp, img_sharp_not, img_zeroone;
    cvtColor(crop, img_gray, CV_BGR2GRAY);
    blur(img_gray, img_gray, Size(4, 4));
    GaussianBlur(img_gray, img_sharp, cv::Size(, ), 6);
    addWeighted(img_gray, 1.80, img_sharp, -0.55, , img_sharp);
    bitwise_not(img_sharp, img_sharp_not);
    threshold(img_sharp_not, img_zeroone, 20, 255, THRESH_BINARY);

کدهای بالا با پردازش تصویر، جادوی زیر رو انجام میده:

مراحل پردازش تصویر ورودی
مراحل پردازش تصویر ورودی

برای قطعه بندی تصاویر؛ یعنی برش تصویر به کاراکترهای جداگانه، ساده ترین روش استفاده از نمودار هیستوگرام (Histogram) و یافتن دسته هایی از پیکسل هاست که در جای خاصی تجمع دارند.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   int histSize = 140;
    Mat histdata, img_sharp_not;
    bitwise_not(img_sharp, img_sharp_not);
    //Calculate the histograms for input image
    reduce(img_sharp_not, histdata, , CV_REDUCE_SUM, CV_32S);
    int hist_w = img.cols * 4; int hist_h = img.rows * 4;
    int bin_w = cvRound((double)hist_w / histSize);
    int txtMargin = 20;
    Mat histImage(hist_h + txtMargin, hist_w + txtMargin, CV_8UC3, Scalar(255, 255, 255));
    //Normalize the result to [ 0, histImage.rows ]
    normalize(histdata, histdata, , histImage.rows, NORM_MINMAX, -1, Mat());
   
    for (int i = 1; i < histSize; i++){    //Draw histogram in summary
        line(histImage, Point(bin_w*(i - 1) + txtMargin, hist_h - cvRound(histdata.at<int>(i - 1))), Point(bin_w*(i) + txtMargin, hist_h - cvRound(histdata.at<int>(i))), Scalar(255, 100, ), 2, 8, );
     }
نمودار هیستوگرام تعداد پیکسل ها برای قطعه بندی تصویر به ازای کپچای ورودی بالا سمت راست نمودار
نمودار هیستوگرام تعداد پیکسل ها برای قطعه بندی تصویر به ازای کپچای ورودی بالا سمت راست نمودار

همون طور که در تصویر هم می بینید نقاطی که افت شدید مقدار داره، دقیقا مکان های متناظری هستند که باید تصویر برش داده بشه تا تصاویر کاراکترها استخراج بشند.

از اونجاییکه این روش با پیچیدگی هایی همراه هست، ما در اینجا با روش خوشه بندی k-means سعی میکنیم تا بهترین مکان برای جداسازی تصویر بعد از فیلتر رو پیدا کنیم. توی این روش از تصویر نهایی به شکل ماتریسی از صفر و یک ها (فقط نقاط سفید) که در مرحله قبل ایجاد کردیم، استفاده میکنیم. پنج نقطه اولیه تصادفی در ماتریس انتخاب میکنیم. با روش k-means سعی میکنیم نقاط درون تصویر رو به پنج خوشه (Cluster) تقسیم کنیم. نقطه مرکز هر خوشه، مرکز هر کاراکتر خواهد بود.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    //Make good representation for clustering
    Mat points = Mat::zeros(sum(img_zeroone)[], 2, CV_32F);
    for (int i = , k = ; i < img_zeroone.rows; i++) {
        for (int j = ; j < img_zeroone.cols; j++) {
            if ((int)img_zeroone.at<char>(i, j) == 255) {
                points.at<float>(k, ) = i;
                points.at<float>(k, 1) = j;
                k++;
            }
        }
    }
    Mat kCenters, kLabels;    //Clustering
    int clusterCount = 5, attempts = 10, iterationNumber = 1e40;
    kmeans(points, clusterCount, kLabels, TermCriteria(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, iterationNumber, 1e-4), attempts, KMEANS_PP_CENTERS, kCenters);
    for (int i = ; i < kCenters.rows; i++) {
        float x = kCenters.at<float>(i, 1), y = kCenters.at<float>(i, );
        circle(img_sharp_not, Point(x, y), 2, (, , 255), -1);
        rectangle(img_sharp_not, Rect(x - 13, y - 13, 26, 26), Scalar(255, 255, 255));
    }

نتیجه کدهای بالا باورنکردنیه:

خروجی خوشه بندی پیکسل ها برای یافتن مرکز کاراکترها و قطعه بندی تصاویر
خروجی خوشه بندی پیکسل ها برای یافتن مرکز کاراکترها و قطعه بندی تصاویر

پردازش های بیشتری هم میشه روی تصویر انجام داد. مثلا برخی تصاویر که کنتراست (Contrast) کمتری دارند، قابلیت نرمال سازی و افزایش کنتراست رو دارند یا برای جداسازی از روش شمارش تجمعی، کاراکترهایی با طول های متفاوت رو جدا کرد. تا همین میزان پردازش برای ایجاد ورودی های شبکه عصبی؛ یعنی همون کاراکترهای جداگانه با اندازه ثابت، کافی است (اگر دوست دارید شما می تونید تا میزان دلخواه پردازش روی تصویر انجام بدید.).

برای مطالعه :   یک مثال عملی با ردیس و پی اچ پی

ساخت و آموزش شبکه عصبی مصنوعی

از اونجاییکه ما فقط نیاز به تشخیص یک تصویر از حرف یا عدد داریم، نیازی به معماری پیچیده برای شبکه عصبی نداریم. شناسایی حروف یک مسئله بسیار ساده تر از تشخیص یک تصویر پیچیده (مثل تصاویری از سگ یا گربه که تنوع زیادی داره.) است. در اینجا ما برای حل مسئله شکستن کپچا از یک معماری شبکه عصبی مصنوعی با دو لایه کانولوشنال (Convolutional) همراه با Max Pooling و دو لایه تماما متصل (Fully-Connected) استفاده می کنیم. اگر شما این شبکه عصبی ها رو نمیشناسید نگران نباشید. در اینجا نیازی به پیاده سازی های سطح پایین نداریم. اگر علاقمند به یادگیری بیشتر در این زمینه هستید، ویکیپدیا یا این کتاب رو بررسی کنید.

معماری شبکه عصبی مصنوعی کانولوشنال برای شکستن کپچا
معماری شبکه عصبی مصنوعی کانولوشنال برای شکستن کپچا

برای سادگی آموزش در اینجا از TensorFlow؛ کتابخونه یادگیری ماشین گوگل، برای ایجاد شبکه استفاده خواهیم کرد. پیاده سازی این مدل به شکل زیر خواهد بود:

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
#conv1
with tf.variable_scope('conv1') as scope:
    kernel = _variable_with_weight_decay('weights', shape=[5, 5, 3, 64], stddev=5e-2, wd=None)
    conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
    biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
    pre_activation = tf.nn.bias_add(conv, biases)
    conv1 = tf.nn.relu(pre_activation, name=scope.name)
    _activation_summary(conv1)
#pool1
pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool1')
#norm1
norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
#conv2
with tf.variable_scope('conv2') as scope:
    kernel = _variable_with_weight_decay('weights', shape=[5, 5, 64, 64], stddev=5e-2, wd=None)
    conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
    biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.1))
    pre_activation = tf.nn.bias_add(conv, biases)
    conv2 = tf.nn.relu(pre_activation, name=scope.name)
    _activation_summary(conv2)
#norm2
norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm2')
#pool2
pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool2')
#FC3
with tf.variable_scope('fc3') as scope:
    reshape = tf.reshape(pool2, [images.get_shape().as_list()[], -1])
    dim = reshape.get_shape()[1].value
    weights = _variable_with_weight_decay('weights', shape=[dim, 384], stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
    fc3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
     _activation_summary(fc3)
#FC4
with tf.variable_scope(fc4') as scope:
    weights = _variable_with_weight_decay('
weights', shape=[384,192], stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('
biases', [192], tf.constant_initializer(0.1))
    fc4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
    _activation_summary(fc4)

اگر بخش های دیگه کد رو هم به این مجموعه اضافه کنید و خوش شانس باشید خروجی زیر رو می بینید:

1
2
3
4
5
2018-04-25 11:45:45.927302: step , loss = 4.68 (2.0 examples/sec; 64.221 sec/batch)
2018-04-25 11:45:49.133065: step 10, loss = 4.66 (533.8 examples/sec; 0.240 sec/batch)
2018-04-25 11:45:51.397710: step 20, loss = 4.64 (597.4 examples/sec; 0.214 sec/batch)
2018-04-25 11:45:54.446850: step 30, loss = 4.62 (391.0 examples/sec; 0.327 sec/batch)
...

داده های آموزشی برای این شبکه نیازمند برچسب هستند، برای اینکار می تونید مدل رو ابتدا با مجموعه تصاویر EMNIST آموزش بدید و بعد با تصاویر اصلی استخراج شده آموزش رو انجام بدید. دقت این مدل رو میشه به ازای داده های ورودی اندازه گرفت، نمودارهای مختلف مثل دقت-سرعت رو در زمان اجرا با استفاده از داشبورد مربوطه رصد و بررسی کرد و کلی کار دیگه که می تونه دقت و سرعت مدل رو افزایش بده. با پیاده سازی مدل یادگیری عمیق، حالا ما می تونیم به صورت خودکار کپچای سیستم گلستان رو بشکنیم!


این مطلب به صورت کامل در گیت هاب در دسترس هست. همچنین اگر علاقمند به موضوعات هوش مصنوعی، یادگیری ماشین و بینایی کامپیوتر هستید می تونید من رو در توییتر دنبال کنید.

ماشین خودکار هوشمند برای بازی کمپین دیجیکالا، تخفیفان و شاتل از توییتر
ماشین خودکار هوشمند برای بازی کمپین دیجیکالا، تخفیفان و شاتل از توییتر

این ماجراجویی تموم شد. حالا نوبت شماست که دست به کار بشید، سیستم تشخیص کپچای دلخواهتون رو طراحی کنید و تجربیاتتون رو به اشتراک بذارید. به نظر شما راهکار جایگزین برای کپچاهای سنتی چه ویژگی هایی باید داشته باشه تا هوش مصنوعی نتونه اون رو بشکنه؟

۲ نظرات

  1. با عرض سلام

    واقعا از این مطلبتون بسیار لذت بردم. این که سعی کردین یک مفهوم تئوری رو با یک مثال جذاب توضیح بدین واقعا ارزشمنده. لطفا همین طور با انرژی ادامه بدین.

     

    با تشکر

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.