Asosiy qismga o'tish

Konvolyutsiya — CV ning yuragi

2-bob 21 kod bloki 2257 so'z

Konvolyutsiya — kompyuter ko'rishning eng muhim amali. Xiralashtirish, chet topish, o'tkirlashtirish — hammasi shu.

Va CNN dagi "C" harfi ham aynan shu.

G'oya: kichik oyna bilan sirg'anish

Rasm ustidan 3x3 oyna sirg'anadi. Har bir joyda:

  1. Oyna ostidagi 9 ta pikselni oladi
  2. Har birini yadro (kernel) dagi songa ko'paytiradi
  3. Hammasini qo'shadi
  4. Natijani markazga yozadi
python4 qator
Rasm bo'lagi        Yadro           Natija
[10  20  30]     [ 0  -1   0]
[40  50  60]  x  [-1   5  -1]  =   50*5 - 20 - 40 - 60 - 80 = evaziga
[70  80  90]     [ 0  -1   0]

Butun sirni yadrodagi sonlar hal qiladi. O'sha 9 ta son rasmga nima bo'lishini belgilaydi.

O'ZINGIZ O'YNANG

Yadrodagi sonlarni o'zgartiring va natijani darhol ko'ring. Tayyor yadrolarni ham sinab ko'ring:

Nimalarga e'tibor bering:

  • Yig'indi = 1 bo'lsa — rasm yorqinligi saqlanadi (xiralashtirish)
  • Yig'indi = 0 bo'lsa — faqat O'ZGARISHLAR qoladi, tekis joylar qora bo'ladi (chet topish)
  • Markaz katta, atrofi manfiy — o'tkirlashtirish

Sobel X ni tanlang: vertikal chiziqlar yorishadi. Sobel Y ni tanlang: gorizontal chiziqlar yorishadi.

Nega? Sobel X chapdan o'ngga o'zgarishni o'lchaydi — vertikal chet aynan shunday o'zgarish beradi.

Kod bilan

python12 qator
import cv2
import numpy as np

rasm = cv2.imread("hujjat.png", cv2.IMREAD_GRAYSCALE)

# Chet topish yadrosi
yadro = np.array([[-1, -1, -1],
                  [-1,  8, -1],
                  [-1, -1, -1]], dtype=np.float32)

natija = cv2.filter2D(rasm, -1, yadro)
cv2.imwrite("chetlar.png", natija)

filter2D(rasm, -1, yadro) — -1 chiqish turi asl bilan bir xil bo'lsin degani (uint8).

Yig'indi nega muhim

python8 qator
yadrolar = {
    "xiralashtirish": np.ones((3,3), np.float32) / 9,
    "chet_topish":    np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]], np.float32),
    "otkirlash":      np.array([[0,-1,0],[-1,5,-1],[0,-1,0]], np.float32),
}

for nom, y in yadrolar.items():
    print(f"{nom:<16} yig'indi = {y.sum():.2f}")

Natija:

python3 qator
xiralashtirish   yig'indi = 1.00
chet_topish      yig'indi = 0.00
otkirlash        yig'indi = 1.00

Yig'indi = 1 → o'rtacha yorqinlik o'zgarmaydi. Yig'indi = 0 → tekis hududlar 0 (qora) bo'ladi, faqat chetlar qoladi.

Agar yadroni o'zingiz yasasangiz va rasm juda yorug' yoki juda qorong'i chiqsa — birinchi navbatda yig'indini tekshiring.

Chegara muammosi

Oyna rasm chetiga yetganda, tashqarida piksel yo'q. Nima qilish kerak?

python4 qator
# Turli chegara siyosatlari
cv2.filter2D(rasm, -1, yadro, borderType=cv2.BORDER_REFLECT)    # aks ettirish
cv2.filter2D(rasm, -1, yadro, borderType=cv2.BORDER_CONSTANT)   # 0 bilan to'ldirish
cv2.filter2D(rasm, -1, yadro, borderType=cv2.BORDER_REPLICATE)  # chetni takrorlash

OpenCV default — BORDER_REFLECT. Neyron tarmoqlarda esa odatda nol bilan to'ldirish (zero padding) ishlatiladi.

Amaliy ta'sir: rasm chetida 1-2 piksellik xato paydo bo'lishi mumkin. Katta rasmda sezilmaydi, kichik rasmda muhim.

Noldan yozish

Kutubxonasiz tushunish uchun:

python14 qator
def konvolyutsiya(rasm, yadro):
    b, k = rasm.shape
    yb, yk = yadro.shape
    p = yb // 2                                  # chegara kengligi

    toldirilgan = np.pad(rasm, p, mode="reflect")
    natija = np.zeros((b, k), dtype=np.float32)

    for y in range(b):
        for x in range(k):
            hudud = toldirilgan[y:y+yb, x:x+yk]
            natija[y, x] = (hudud * yadro).sum()

    return np.clip(natija, 0, 255).astype(np.uint8)

(hudud * yadro).sum() — butun konvolyutsiya shu bitta qatorda. Element-wise ko'paytirish, keyin yig'indi.

np.clip(..., 0, 255) — natija manfiy yoki 255 dan katta bo'lishi mumkin. Qirqmasak, uint8 ga aylantirishda son "aylanib ketadi": 300 → 44.

Tekshiring:

python9 qator
rasm = cv2.imread("hujjat.png", cv2.IMREAD_GRAYSCALE)
yadro = np.array([[0,-1,0],[-1,5,-1],[0,-1,0]], np.float32)

meniki = konvolyutsiya(rasm, yadro)
opencv = cv2.filter2D(rasm, -1, yadro)

farq = np.abs(meniki.astype(int) - opencv.astype(int))
print(f"o'rtacha farq: {farq.mean():.2f}")
print(f"maksimal farq: {farq.max()}")

Natija:

matn2 qator
o'rtacha farq: 0.00
maksimal farq: 0

Aniq bir xil. Chunki biz OpenCV bilan bir xil chegara siyosatini (reflect) ishlatdik. Boshqa siyosat tanlasangiz — rasm chetida kichik farq paydo bo'ladi.

Lekin sekinligiga e'tibor bering. Ikkita ichma-ich sikl — Python uchun juda sekin. cv2.filter2D C++ da yozilgan va SIMD ishlatadi, shuning uchun yuzlab barobar tez.

Noldan yozish — TUSHUNISH uchun. Ishlatish uchun kutubxona.

im2col — nega kutubxona 100 barobar tez

Yuqorida o'z konvolyutsiyamiz sekin ekanini ko'rdik. Sabab — ikkita ichma-ich sikl. Lekin cv2.filter2D ham xuddi shu ishni qiladi. Nega u tez?

Javob: u konvolyutsiyani KO'PAYTIRISHGA aylantiradi.

Kompyuterlar matritsa ko'paytirishni juda tez bajaradi — buning uchun maxsus kutubxonalar (BLAS) va protsessor buyruqlari bor. Ichma-ich Python sikli esa eng sekin yo'l.

G'oya

Har bir 3x3 oynani bitta qatorga yoyamiz:

python3 qator
Oyna:  [a b c]        Qator: [a b c d e f g h i]
       [d e f]   ->
       [g h i]

Barcha oynalarni shunday yoysak — katta matritsa chiqadi. Yadroni ham ustunga yoysak — natija oddiy matritsa ko'paytirish bo'ladi.

python20 qator
def im2col(rasm, yadro_olcham):
    """Har bir oynani alohida QATORga yoyadi."""
    b, k = rasm.shape
    y = yadro_olcham
    chiqish_b, chiqish_k = b - y + 1, k - y + 1

    ustunlar = np.zeros((chiqish_b * chiqish_k, y * y), dtype=np.float32)
    idx = 0
    for i in range(chiqish_b):
        for j in range(chiqish_k):
            ustunlar[idx] = rasm[i:i+y, j:j+y].ravel()
            idx += 1
    return ustunlar, (chiqish_b, chiqish_k)


def konv_im2col(rasm, yadro):
    """Konvolyutsiya = im2col + matritsa ko'paytirish."""
    ustunlar, shakl = im2col(rasm.astype(np.float32), yadro.shape[0])
    natija = ustunlar @ yadro.ravel()          # BITTA ko'paytirish
    return natija.reshape(shakl)

ustunlar @ yadro.ravel() — butun konvolyutsiya shu bitta amalda. Sikl yo'q.

Tekshiramiz

python21 qator
import time

rasm = np.random.randint(0, 255, (200, 200)).astype(np.uint8)
yadro = np.array([[0,-1,0],[-1,5,-1],[0,-1,0]], np.float32)

t0 = time.perf_counter()
a = konvolyutsiya(rasm, yadro)                  # sikl bilan
t_sikl = time.perf_counter() - t0

t0 = time.perf_counter()
b = konv_im2col(rasm, yadro)
t_im2col = time.perf_counter() - t0

t0 = time.perf_counter()
c = cv2.filter2D(rasm, cv2.CV_32F, yadro)
t_cv = time.perf_counter() - t0

print(f"sikl   : {t_sikl*1000:>8.1f} ms")
print(f"im2col : {t_im2col*1000:>8.1f} ms")
print(f"OpenCV : {t_cv*1000:>8.1f} ms")
print(f"im2col sikldan {t_sikl/t_im2col:.0f} barobar tez")

Natija (200x200 rasm):

matn4 qator
sikl   :     47.2 ms
im2col :     19.9 ms
OpenCV :      0.1 ms
im2col sikldan 2 barobar tez

Kutilmagan natija — im2col atigi 2 barobar tez, OpenCV esa 472 barobar. Nega?

Chunki bizning im2col hali ham Python siklidan foydalanadi — matritsani qurish uchun. Biz siklni yo'q qilmadik, faqat uni ko'chirdik: konvolyutsiyadan matritsa qurishga.

Faqat oxirgi @ amali tez bo'ldi.

Haqiqiy kutubxonalarda im2col ham C da yozilgan — u yerda sikl yo'q. Shuning uchun OpenCV 0.1 ms da bajaradi.

Saboq: tezlik faqat algoritmdan emas, bajarilish tilidan ham keladi. Python da yozilgan har qanday piksel-sikl sekin bo'ladi.

Narxi: XOTIRA

im2col bepul emas. 3x3 yadro uchun har bir piksel 9 marta nusxalanadi.

python5 qator
rasm = np.zeros((200, 200), np.uint8)
ustunlar, _ = im2col(rasm.astype(np.float32), 3)
print(f"asl rasm : {rasm.nbytes:,} bayt")
print(f"im2col   : {ustunlar.nbytes:,} bayt")
print(f"o'sish   : {ustunlar.nbytes / rasm.nbytes:.0f}x")

Natija:

matn3 qator
asl rasm : 40,000 bayt
im2col   : 1,411,344 bayt
o'sish   : 35x

35 barobar — 9 emas. Ikkita sabab ko'payadi: 9 marta nusxalash x float32 (4 bayt) va uint8 (1 bayt) farqi = 36. Deyarli aynan shu.

Bu klassik almashuv: xotira evaziga tezlik.

PyTorch va TensorFlow ham shu usulni ishlatadi (yoki undan ham tezroq — Winograd, FFT). Shuning uchun nn.Conv2d tez.

Receptive field — neyron nimani KO'RADI

Bitta 3x3 konvolyutsiyadan keyin har bir chiqish pikseli asl rasmning 3x3 hududini "ko'radi".

Ikkinchi 3x3 qatlamdan keyin-chi?

python4 qator
1-qatlam: har bir piksel 3x3 ko'radi
2-qatlam: uning kirishidagi har bir piksel ham 3x3 ko'radi
       -> jami 5x5
3-qatlam -> 7x7

Bu receptive field (qabul maydoni) deyiladi.

python12 qator
def receptive_field(qatlamlar, yadro=3, stride=1):
    """Nechta qatlamdan keyin neyron qancha hududni ko'radi."""
    rf = 1
    jump = 1
    for _ in range(qatlamlar):
        rf = rf + (yadro - 1) * jump
        jump = jump * stride
    return rf


for n in (1, 2, 3, 5, 10):
    print(f"{n:>2} qatlam (3x3, stride 1) -> {receptive_field(n)}x{receptive_field(n)}")

Natija:

matn5 qator
 1 qatlam -> 3x3
 2 qatlam -> 5x5
 3 qatlam -> 7x7
 5 qatlam -> 11x11
10 qatlam -> 21x21

Har bir qatlam atigi 2 piksel qo'shadi. 10 qatlamdan keyin ham faqat 21x21. Bu juda sekin o'sish.

Nega bu MUHIM

Model faqat receptive field ichidagi narsani ko'ra oladi.

Agar hujjatdagi jadval 200x200 piksel bo'lsa, lekin tarmoqning receptive field i 15x15 bo'lsa — u jadvalni butun holda hech qachon ko'rmaydi. Faqat kichik bo'laklarni.

Shuning uchun chuqur tarmoq kerak: har bir qatlam ko'rish doirasini kengaytiradi.

Pooling receptive field ni TEZ o'stiradi

python13 qator
def rf_pooling_bilan(bloklar):
    """Har blok: conv3x3 + conv3x3 + maxpool2 (VGG uslubi)."""
    rf, jump = 1, 1
    for _ in range(bloklar):
        for _ in range(2):                 # ikkita conv
            rf += 2 * jump
        rf += 1 * jump                     # pool 2x2
        jump *= 2                          # pool stride 2
    return rf


for b in (1, 2, 3, 4):
    print(f"{b} blok -> receptive field {rf_pooling_bilan(b)}")

Natija:

matn4 qator
1 blok -> 6
2 blok -> 16
3 blok -> 36
4 blok -> 76

4 blokda 76x76 — yuqoridagi 10 qatlamli variant esa atigi 21x21 bergan edi. Farq: pooling.

jump *= 2 — pooling dan keyin har bir qadam asl rasmda ikki barobar kattaroq masofani bosadi. Shuning uchun receptive field eksponensial o'sadi.

Faqat konvolyutsiya bilan 10 qatlam kerak bo'lgan joyda, pooling bilan 3 blok yetadi.

VGG qoidasi: ikkita 3x3 = bitta 5x5

python4 qator
bitta_5x5 = 5 * 5
ikkita_3x3 = 2 * (3 * 3)
print(f"bitta 5x5 yadro : {bitta_5x5} parametr, RF 5x5")
print(f"ikkita 3x3      : {ikkita_3x3} parametr, RF {receptive_field(2)}x{receptive_field(2)}")

Natija:

matn2 qator
bitta 5x5 yadro : 25 parametr, RF 5x5
ikkita 3x3      : 18 parametr, RF 5x5

Bir xil receptive field, kamroq parametr — va orada qo'shimcha ReLU bor, ya'ni ko'proq nochiziqlilik.

Mana shuning uchun zamonaviy tarmoqlarda deyarli faqat 3x3 yadrolar ishlatiladi. 4-bobdagi kodimizda ham shunday.

CNN bilan bog'lanish

Yuqorida yadrolarni qo'lda yozdik: chet topish uchun shunday, xiralashtirish uchun bunday.

CNN da esa yadrolar o'rganiladi. Tarmoq minglab rasmni ko'rib, "qanday yadro foydali" ekanini o'zi topadi.

Birinchi qatlamlar odatda chet va burchak topadigan yadrolarni o'rganadi — biz qo'lda yozganiga juda o'xshash. Chuqurroq qatlamlar esa murakkabroq shakllarni.

Shuning uchun bu bob muhim: CNN sehr emas, u o'rganilgan konvolyutsiya.


Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar