Asosiy qismga o'tish

CNN — o'rganiladigan konvolyutsiya

4-bob 15 kod bloki 1481 so'z

2-bobda yadrolarni qo'lda yozdik: chet topish uchun shunday, xiralashtirish uchun bunday.

CNN da yadrolar o'rganiladi. Boshida tasodifiy sonlar, keyin tarmoq minglab rasmni ko'rib, foydali yadrolarni o'zi topadi.

Nima uchun oddiy tarmoq yetmaydi

100x100 rangli rasmni to'liq bog'langan (fully connected) qatlamga bersak:

python3 qator
kirish = 100 * 100 * 3          # 30 000 piksel
neyronlar = 128
print(f"vaznlar soni: {kirish * neyronlar:,}")

3.8 million vazn — bitta qatlam uchun. Va bu hali boshlanishi.

Uch muammo bor:

  1. Juda ko'p parametr — o'qitish uchun juda ko'p ma'lumot kerak
  2. Joylashuv yo'qoladi — qo'shni piksellar bog'liqligi hisobga olinmaydi, rasm shunchaki 30 000 ta alohida son bo'ladi
  3. Siljish muammosi — mushuk chapda bo'lsa boshqa neyronlar, o'ngda bo'lsa boshqa neyronlar yonadi

CNN uchalasini ham hal qiladi.

Uchta g'oya

1. Lokal bog'lanish. Har bir neyron butun rasmni emas, kichik hududni (masalan 3x3) ko'radi.

2. Vazn ulashish (weight sharing). Bitta yadro butun rasm bo'ylab sirg'anadi. Chapdagi chetni topadigan yadro o'ngdagi chetni ham topadi — bir xil vaznlar bilan.

python9 qator
# To'liq bog'langan: har bir piksel uchun alohida vazn
fc_vazn = 100 * 100 * 3 * 128

# Konvolyutsion: faqat yadro vaznlari
conv_vazn = 3 * 3 * 3 * 128        # 3x3 yadro, 3 kanal, 128 filtr

print(f"FC   : {fc_vazn:,}")
print(f"Conv : {conv_vazn:,}")
print(f"kamayish: {fc_vazn // conv_vazn} barobar")

Natija:

matn3 qator
FC   : 3,840,000
Conv : 3,456
kamayish: 1111 barobar

Ming barobardan ko'p kam parametr — va natija yaxshiroq. Chunki vazn ulashish tarmoqni "chet har joyda chet" degan haqiqatni o'rganishga majbur qiladi.

3. Pooling. O'lchamni kichraytirish, muhimini saqlash.

PyTorch da qatlamlar

python11 qator
import torch
import torch.nn as nn

x = torch.randn(1, 3, 64, 64)      # (batch, kanal, balandlik, kenglik)

conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
chiqish = conv(x)

print("kirish :", tuple(x.shape))
print("chiqish:", tuple(chiqish.shape))
print("vaznlar:", sum(p.numel() for p in conv.parameters()))

Natija:

yaml3 qator
kirish : (1, 3, 64, 64)
chiqish: (1, 16, 64, 64)
vaznlar: 448

448 = 16 filtr x (3 kanal x 3 x 3 yadro) + 16 bias. Kanallar soni 3 dan 16 ga o'sdi, o'lcham esa padding=1 tufayli saqlandi.

(1, 3, 64, 64) — PyTorch tartibi: batch, kanal, balandlik, kenglik.

Diqqat: OpenCV da (balandlik, kenglik, kanal), PyTorch da (kanal, balandlik, kenglik). Kanal o'rni har xil. Bu eng ko'p uchraydigan xato.

O'tkazish:

python6 qator
import numpy as np

rasm_cv = np.zeros((64, 64, 3), dtype=np.uint8)     # OpenCV: HWC
tensor = torch.from_numpy(rasm_cv).permute(2, 0, 1)  # PyTorch: CHW
print("OpenCV :", rasm_cv.shape)
print("PyTorch:", tuple(tensor.shape))

padding — o'lcham nega o'zgaradi

python5 qator
x = torch.randn(1, 1, 32, 32)

for p in (0, 1):
    c = nn.Conv2d(1, 1, kernel_size=3, padding=p)
    print(f"padding={p} -> {tuple(c(x).shape)}")

Natija:

python2 qator
padding=0 -> (1, 1, 30, 30)
padding=1 -> (1, 1, 32, 32)

padding=0 — har konvolyutsiyada 2 piksel yo'qoladi (3x3 yadro uchun). 10 qatlamdan keyin 20 piksel yo'q bo'ladi.

padding=1 — o'lcham saqlanadi. Chuqur tarmoqlarda deyarli doim shu ishlatiladi.

Formula:

python1 qator
chiqish = (kirish + 2*padding - yadro) / stride + 1

Pooling — kichraytirish

python3 qator
x = torch.randn(1, 16, 32, 32)
pool = nn.MaxPool2d(kernel_size=2, stride=2)
print(tuple(pool(x).shape))         # (1, 16, 16, 16)

MaxPool 2x2 hududdan eng kattasini oladi. O'lcham yarmiga tushadi, kanallar soni o'zgarmaydi.

Nega maksimum? Chunki bizni "shu hududda belgi BORMI?" degan savol qiziqtiradi. Eng kuchli javob — maksimum.

LeNet — birinchi ishlaydigan CNN (1998)

python29 qator
class LeNet(nn.Module):
    def __init__(self, sinflar=10):
        super().__init__()
        self.belgilar = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, padding=2),   # 28x28 -> 28x28
            nn.ReLU(),
            nn.MaxPool2d(2),                              # -> 14x14
            nn.Conv2d(6, 16, kernel_size=5),              # -> 10x10
            nn.ReLU(),
            nn.MaxPool2d(2),                              # -> 5x5
        )
        self.klassifikator = nn.Sequential(
            nn.Flatten(),
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU(),
            nn.Linear(120, 84),
            nn.ReLU(),
            nn.Linear(84, sinflar),
        )

    def forward(self, x):
        x = self.belgilar(x)
        return self.klassifikator(x)


model = LeNet()
x = torch.randn(4, 1, 28, 28)
print("chiqish:", tuple(model(x).shape))
print("parametr:", sum(p.numel() for p in model.parameters()))

Natija:

yaml2 qator
chiqish: (4, 10)
parametr: 61706

61 706 parametr — bugungi standartlar bo'yicha juda kichik. ResNet-50 da 25 million bor. Lekin LeNet 1998 yilda qo'lyozma raqamlarni tanishni hal qilgan edi.

(4, 10) — 4 ta rasm uchun 10 tadan ball (10 ta sinf).

Tuzilish har doim bir xil: konvolyutsiya bloklari belgilarni ajratadi, oxirida to'liq bog'langan qatlamlar qaror qabul qiladi.

16 * 5 * 5 qayerdan keldi? Oxirgi konvolyutsiya 16 kanal, 5x5 o'lcham qoldirdi. Flatten ularni 400 ta songa yoyadi.

Bu sonni qo'lda hisoblash kerak — va o'lcham o'zgarsa xato chiqadi. Shuning uchun ko'p kod nn.AdaptiveAvgPool2d ishlatadi: u kirish o'lchamidan qat'i nazar bir xil chiqish beradi.

ResNet — chuqurlik muammosi

LeNet 5 qatlam. Zamonaviy tarmoqlar 50, 100, 150 qatlam.

Lekin 2015 gacha chuqur tarmoqlar yomonroq ishlardi. 56 qatlamli tarmoq 20 qatlamlidan ko'ra ko'proq xato qilardi — o'qitishda ham, testda ham.

Sabab: gradient yo'qolishi. Orqaga tarqalishda gradient har qatlamdan o'tganda kichrayadi. 50 qatlamdan keyin deyarli nol — birinchi qatlamlar o'rganmaydi.

Yechim: skip connection (o'tkazib yuborish).

python14 qator
class QoldiqBlok(nn.Module):
    def __init__(self, kanal):
        super().__init__()
        self.yol = nn.Sequential(
            nn.Conv2d(kanal, kanal, 3, padding=1),
            nn.BatchNorm2d(kanal),
            nn.ReLU(),
            nn.Conv2d(kanal, kanal, 3, padding=1),
            nn.BatchNorm2d(kanal),
        )
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.relu(self.yol(x) + x)      # <-- + x : SKIP

self.yol(x) + x — butun ResNet g'oyasi shu qo'shuvda.

Nega ishlaydi: gradient endi ikki yo'ldan oqadi — konvolyutsiyalar orqali VA to'g'ridan-to'g'ri + x orqali. Ikkinchi yo'lda gradient kichraymaydi.

Boshqacha aytganda: blok butun natijani emas, faqat o'zgarishni (qoldiqni) o'rganadi. Agar blok foydasiz bo'lsa, u nolga yaqinlashadi va x o'zgarmasdan o'tadi.

python4 qator
blok = QoldiqBlok(16)
x = torch.randn(2, 16, 32, 32)
print("kirish :", tuple(x.shape))
print("chiqish:", tuple(blok(x).shape))

Kirish va chiqish bir xil o'lcham — aks holda + x ishlamaydi. Kanal soni o'zgarganda 1x1 konvolyutsiya bilan moslashtiriladi.

BatchNorm nega bor

python2 qator
# parcha
nn.BatchNorm2d(kanal)

Har bir batch uchun chiqishni normallashtiradi (o'rtacha 0, dispersiya 1). Natija:

  • O'qitish tezroq boradi
  • Kattaroq learning rate ishlatish mumkin
  • Boshlang'ich vaznlarga kam bog'liq

Deyarli har bir zamonaviy CNN da bor.

Belgilar qanday o'rganiladi

O'qitilgan tarmoqning birinchi qatlam yadrolariga qarasangiz — ular 2-bobdagi qo'lda yozganimizga juda o'xshash: chet topish, burchak topish, rang o'tishlari.

Hech kim ularni dasturlamagan. Tarmoq o'zi topgan, chunki ular foydali.

Chuqurroq qatlamlar murakkabroq narsalarni o'rganadi:

Qatlam Nimani topadi
1–2 chetlar, burchaklar, ranglar
3–4 tekstura, oddiy shakllar
5+ obyekt qismlari (g'ildirak, ko'z)
oxirgi butun obyekt

Shuning uchun transfer learning ishlaydi: birinchi qatlamlar har qanday rasm uchun foydali. Buni keyingi bobda ishlatamiz.


Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar