در شبکه های عمیق برای کاهش تعداد پارامتر ها ازmax pooling استفاده می شود به این صورت که بعد از چند لایه کانولوشنی ابعاد آن لایه های کانولوشنی را تغییر میدهد.
هایپرپارامتر هایی Pooling
- stride
- kernel size
- average or max بودن تابع
درشبکه های عمیق و در لایه های fully connected پارامتر های این لایه ها به سایز ورودی وابسته بود ولی در لایه های کانولوشنی تعداد پارامتر ها تنها به اندازه و تعداد فیلتر بستگی دارد. برای مثال وقتی مانند شکل زیر لایه آخر5 در 5 در 100 باشد (feature map تصویر) و به یک لایه fully connected صد تایی وصل میکنیم.اگر تصویر ورودی مقداری بزرگتر شود این لایه 100*5*5 تغییر می کند مثلا سایز آن 100*7*7 می شود ، پس تعداد وزن هایی که به نورون های لایه آخر وصل می شوند تغییر می کند.

مشکل : تعداد وزن های لایه flat و fully conected
راه حل : global average pooling(GAP)
روش کار

در مقاله [Lin et al., 2013. Network in network] به جای fully connected ها در انتها global average pooling میزنیم تا future map ها را مستقیم به softmax وصل کنیم و از fully connected استفاده نکنیم.

لایه آخر را pooling با kernel هم سایز تصویر ورودی (activation map) میذاریم تا درانتها تنها یک کانال باقی بماند، یعنی طول و عرض هرچه باشد 1 شده و عمقش همان باقی می ماند. حال همین لایه را مستقیم به fully connected وصل می کنیم.
ResNet50 هم به صورت غیر مستقیم از average pooling استفاده کرده. به این صورت که لایه pooling آخرش اندازه خروجی (2048 * 1* 1) شده است ولی از fully connected ها هم برای کلاس بندی انتهایی استفاده کرده است.
بررسی لایه های آخر ResNet50

در لایه های انتهایی شبکه به دلیل وجود , Activation ، AveragePooling2D و لایه Dense برای ما مهم هستند.
- ابتدا لایه activation را بررسی می کنیم؛ این لایه شامل2048 نقشه فعال سازی (activation map ) که سایز آن (7 * 7) است، می باشد.در تصویر بالا k همان تعداد activation map ها است.
k ={1, 2, 3, …., 2048}
- لایه (GAP) AveragePooling2D سایز هر کدام از این activation map ها را از (7 * 7) به (1 * 1) کاهش می دهد.
- لایهDense که صرفا ورودی را صاف کرده و بدون تغییر در اطلاعاتی که در لایه قبلی GAP ایجاد شده است تنها دسته یا کلاس آن خروجی را مشخص می کند. حال برای اینکه شبکه هایی که طراحی می کنیم به سایز ورودی وابسته نباشند می توان از این لایه صرف نظر کرد و لایه GAP را مستقیما به softmax متصل کرد.
مزایا :
- متناسب تر با شبکه های کانولوشنی است. زیرا با ساختار convolution ها بیشتر دارد. fully connected ساختار کانولوشن را به هم می ریزد. ارتباط بین دسته بندی لایه آخر و feature map ها حفظ می کند.
- از overfitting جلوگیری می کند. : هیچ پارامتری برای بهینه سازی ندارد واز overfitting هم دراین لایه جلوگیری می کند. و می توان با کانولوشن بیشتر ، کاری کنیم عمق افزایش یافته و داده کمتری از دست برود.
- به جابجایی حساس نیست. : نسبت به جابجایی های تصویر عملکرد بهتری دارد. مثلا شبکه ایی برای شناسایی سگ داریم و دیگر جایگاه سگ در هرجای تصویراهمیتی ندارد.
استفاده از global average pooling در framework های مختلف
در keras کد global average pooling به صورت زیر است که تنها dataform را میگیرد و سایز ورودی نمی خواهد چون نسبت به سایز activation map سایز آن را انتخاب میکند.
|
keras.layers.GlobalMaxPooling2D(data_format=None) |
pytorch
از بخوایم global pooling در pytorch داشته باشیم adaptivePooling با سایز یک را صدا میزنیم. اینگونه عمل می کند که سایز خروجی رابهش به عنوان پارامتر می دهیم.
|
torch.nn.AdaptiveAvgPool2d(output_size) |
|
>>> # target output size of 5x7 >>> m = nn.AdaptiveMaxPool2d((5,7)) >>> input = torch.randn(1, 64, 8, 9) >>> output = m(input) >>> # target output size of 7x7 (square) >>> m = nn.AdaptiveMaxPool2d(7) >>> input = torch.randn(1, 64, 10, 9) >>> output = m(input) >>> # target output size of 10x7 >>> m = nn.AdaptiveMaxPool2d((None, 7)) >>> input = torch.randn(1, 64, 10, 9) >>> output = m(input) |
fastai
به این صورت پیاده سازی شده است که AdaptiveAvgPool2d و AdaptiveMaxPool2d را صدا میزند و خروجی این دو تابع را بهم متصل می کند.
|
class AdaptiveConcatPool2d(nn.Module): "Layer that concats `AdaptiveAvgPool2d` and `AdaptiveMaxPool2d`." def __init__(self, sz:Optional[int]=None): "Output will be 2*sz or 2 if sz is None" super().__init__() sz = sz or 1 self.ap,self.mp = nn.AdaptiveAvgPool2d(sz), nn.AdaptiveMaxPool2d(sz) def forward(self, x): return torch.cat([self.mp(x), self.ap(x)], 1) |
مسئله : تشخیص نژاد های مختلف سگ
دیتا شامل پوشه عکس ها و یک فایل اکسل شامل نام عکس و نژادش است.
|
PATH = "D:/dataset/dog-breed-identification/" sz = 224 arch = resnext101_64 bs = 58 |
در کد بالا آدرس پوشه عکس ها را داده خط 2 سایز ورودی را مشخص کرده در خط 3 شبکه را resnex124 تعریف کرده ایم و خط 4 batch size رو هم 58 گذاشتم.
(اگر روی gpu کارکنید و 2 یا 3 گیگ رم دارید احتمالا باید تعداد batch size را کم کنید.)
|
label_csv = f'{PATH}labels.csv' n = len(list(open(label_csv))) - 1 # header is not counted (-1) val_idxs = get_cv_idxs(n) # random 20% data for validation set |
لیبل ها را از فایل csv خونده واون label باز کن و لیستش کن و طول اون لیست رو به ما بعده منهای یک برای جدا کردن هدر لیست که همان تعداده داده های ماست، توسط تابع get_cv_index(n) ایندکس های cross validation را جدا می کند اینگونه که 20 درصد index ها را برای cross validation جدا میکند.
|
import os os.listdir(PATH) |
کد بالا نشان می دهد تا اینجا دو پوشه train و test همراه با label ها جدا شده است.
|
label_df = pd.read_csv(label_csv) label_df.head() |
فایل csv را با پاندا خونده و 5تای اولشو نمایش می دهد.
|
label_df.pivot_table(index="breed", aggfunc=len).sort_values('id', ascending=False) |
تعداد هر کلاس را پیدا کرده ایم.
تابع pivot_table :
|
tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, max_zoom=1.1) data = ImageClassifierData.from_csv(PATH, 'train', f'{PATH}labels.csv', test_name='test', # we need to specify where the test set is if you want to submit to Kaggle competitions val_idxs=val_idxs, suffix='.jpg', tfms=tfms, bs=bs) |
به تابع tfms_from_model معماری ، سایز ورودی، ارگومیشن هم میخواهیم به صورت side_on ، بیشترین زوم هم 1.1 باشه بر روی تصاویر(چقدر روی عکس هربار رندوم زون کند)
تابع ImageClassifierData.from_csv می گوید از همان ساختاری که عکس های توی یک پوشه هستند و لیبل ها در csv استفاده کن. مسیر پوشه ها و اسم پوشه های train و test را گرفته و اگر valdation_index بهش پاس بدیم توی train_set اونا رو در نظر نمیگیره. توی اکسل پسوند اسم عکس ها وجود نداشته و به عنوان suffix پسوند پاس داده شده.
|
size_d = {k: PIL.Image.open(PATH + k).size for k in data.trn_ds.fnames} |
بر روی تمام فایل ها سایز ها گرفته و تمام سایز ها را در size_d دارد
|
row_sz, col_sz = list(zip(*size_d.values())) row_sz = np.array(row_sz); col_sz = np.array(col_sz) plt.hist(row_sz); |
سایز سطر و ستون را جدا کرده و به صورت هیستوگرام نمایش می دهد. که هیستوگرام نشان می دهد بیشتر تصاویر سایزی در اندازه 500 تا 1000 و کمتر از هزار هستند. پس تصاویر من در اندازه های بزرگ اند.
|
def get_data(sz, bs): # sz: image size, bs: batch size tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, max_zoom=1.1) data = ImageClassifierData.from_csv(PATH, 'train', f'{PATH}labels.csv', test_name='test', val_idxs=val_idxs, suffix='.jpg', tfms=tfms, bs=bs) # http://forums.fast.ai/t/how-to-train-on-the-full-dataset-using-imageclassifierdata-from-csv/7761/13 # http://forums.fast.ai/t/how-to-train-on-the-full-dataset-using-imageclassifierdata-from-csv/7761/37 return data if sz > 300 else data.resize(340, 'tmp') # Reading the jpgs and resizing is slow for big images, so resizing them all to 340 first saves time data = get_data(sz, bs) |
تابع فوق داده ها را برمیگرداند با سایز تصویر و batch size ورودی دلخواه. هروقت تابع صدا شود یک trasformation میزند و سپس تصاویر را به اندازه batch size در متغییر data ریخته و در انتها قبل از بازگردانی تصاویر کوچکتر از 300 را ابتدا به 340 تبدیل کرده و سپس در پوشه tmp ذخیره میکند که حداقل تغییر اندازه از 340 باشد به اون سایز که cashing یک بارانجام شده باشد تا در تغییر اندازه تصاویر خیلی بزرگ به سایز کوچک overhead کمتری داشته باشیم و سرعت افزایش بیابد.(چون میخواهیم هر epoch را با یه سایزی پیش بریم)
|
learn = ConvLearner.pretrained(arch, data, precompute=True) |
از ConvLearner متد pretrained را صدا زده و معماری و داده و پارامتر precompute هم true قرار میدهد.
متدpretrained درای پارامتر ps است که بیانگر احتمال dropout است.
(در هنگام اجرای کد بالا با خطا مواجهه شدید وزن هارا دانلودکرده و در پوشه اشاره شده قرار دهید.)
با learning rate ده به توان منفی 2 ، به تعداد 5 epoch می زند. با سایز وردوی 224 در 224
precompute را false کرده تا data argumation ما فعال بشه.
|
learn.fit(1e-2, 5, cycle_len= 1) |
cycle_len= 1 بیانگر این است که یکی یکی بره بالا
|
Learn.set_data(get_data(299, bs)) Learn.freeze() |
با set_data شبکه رو آگاه! میکنه که سایز عکس قراره تغییرکنه. سپس شبکه را فیریز می کند.

دیده می شود که بعد از adavtivePooling سایز ورودی از 2048 به سایز 4069 تغییر یافته است.
|
log_preds_y = Learn.TTA() probs = np.mean(np.exp(log_preds), 0) accuracy_np(probs, y) , metricslog_loss(y, probs) |
باTTA() می خواهد ببیند که می توان validation را بهتر کرد ..
مدل را ذخیره می کند.
شبکه های اجتماعی