【PyTorch】Macro Soft F1 Lossを実装する
マルチラベル問題の評価指標の一つにMacro F1というものがあります。 Macro F1はそのままでは微分できないのでロス関数には適さないのですが、評価指標を微分可能にしてロス関数にしてしまおうという考えもあるようです。
リンクではMacro F1をロス関数に適用出来るようにしたMacro Soft F1 LossのKeras実装があるのですが、PyTorch版を実装しました。sigmoid + binary crossentropyと比較するとロスと正解率が結びつきやすいのは利点ですが、バッチサイズを十分に取らないといけなさそうなロス関数という印象です。
import torch import torch.nn as nn class MacroSoftF1Loss(nn.Module): def __init__(self, consider_true_negative, sigmoid_is_applied_to_input): super(MacroSoftF1Loss, self).__init__() self._consider_true_negative = consider_true_negative self._sigmoid_is_applied_to_input = sigmoid_is_applied_to_input def forward(self, input_, target): target = target.float() if self._sigmoid_is_applied_to_input: input = input_ else: input = torch.sigmoid(input_) TP = torch.sum(input * target, dim=0) FP = torch.sum((1 - input) * target, dim=0) FN = torch.sum(input * (1 - target), dim=0) F1_class1 = 2 * TP / (2 * TP + FP + FN + 1e-8) loss_class1 = 1 - F1_class1 if self._consider_true_negative: TN = torch.sum((1 - input) * (1 - target), dim=0) F1_class0 = 2*TN/(2*TN + FP + FN + 1e-8) loss_class0 = 1 - F1_class0 loss = (loss_class0 + loss_class1)*0.5 else: loss = loss_class1 macro_loss = loss.mean() return macro_loss
【PyTorch】安物GPUだけどバッチサイズ大きくしたい
諸事情によりバッチサイズを大きく取らないといけなくなったのですが、そんな時はoptimizerのstep等のタイミングを変更することで同等のことができそうです。 例えば下記の疑似コードでバッチサイズ16, accumulation=2であればバッチサイズ32で実行しているかのようになります。
optimizer.zero_grad() for i, (data, target) in enumerate(data_loader): output = model(data) loss = criterion(output, target) loss.backward() # accumulation分蓄積するまでstepしない if (i+1)%accumulation == 0: optimizer.step() optimizer.zero_grad()
【PyTorch】マルチラベル問題で使われているFocalLossを見つけたのでメモ
マルチラベル+不均衡データを扱うのでマルチラベル問題で利用されているFocalLossの実装を探したのですが見つけました。感謝!
import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma=2): super(FocalLoss, self).__init__() self.gamma = gamma def forward(self, input, target): target = target.float() # BCELossWithLogits max_val = (-input).clamp(min=0) loss = input - input * target + max_val + \ ((-max_val).exp() + (-input - max_val).exp()).log() invprobs = F.logsigmoid(-input * (target * 2.0 - 1.0)) loss = (invprobs * self.gamma).exp() * loss if len(loss.size()) == 2: loss = loss.sum(dim=1) return loss.mean()
【Python】データ拡張ライブラリAlbumentationsの設定保存・復元
ディープラーニングで実験するときにどんなデータ拡張を利用したのかファイルとして保存しておきたかったのですが、 データ拡張ライブラリAlbumentationsの最新版には既にその機能があったのでメモします。
from albumentations import Compose from albumentations.augmentations.transforms import Resize, HorizontalFlip, RandomSizedCrop, HueSaturationValue from albumentations.core.serialization import save, load def get_compose(crop_min_max, image_height, image_width, hue_shift, saturation_shift, value_shift): return Compose([Resize(image_height, image_width, p=1.0), HorizontalFlip(p=0.5), RandomSizedCrop(crop_min_max, image_height, image_width, p=1.0), HueSaturationValue(hue_shift, saturation_shift, value_shift, p=1.0)]) # Resize image to 256x256 image_size = 256 # Crop 80 - 100% of image crop_min = image_size*80//100 crop_max = image_size crop_min_max = (crop_min, crop_max) # HSV shift limits hue_shift = 10 saturation_shift = 10 value_shift = 10 # Get compose compose = get_compose(crop_min_max, image_size, image_size, hue_shift, saturation_shift, value_shift) # Save compose in yaml format save(compose, 'data_augmentations.yaml', data_format='yaml') # Load compose from file compose = load('data_augmentations.yaml', data_format='yaml') print(compose)
data_augmentations.yamlにはこんな感じで保存されています。 一応ファイルを人目で見ても可読かと思います。
__version__: 0.4.3 transform: __class_fullname__: albumentations.core.composition.Compose additional_targets: {} bbox_params: null keypoint_params: null p: 1.0 transforms: - __class_fullname__: albumentations.augmentations.transforms.Resize always_apply: false height: 256 interpolation: 1 p: 1.0 width: 256 - __class_fullname__: albumentations.augmentations.transforms.HorizontalFlip always_apply: false p: 0.5 - __class_fullname__: albumentations.augmentations.transforms.RandomSizedCrop always_apply: false height: 256 interpolation: 1 min_max_height: - 204 - 256 p: 1.0 w2h_ratio: 1.0 width: 256 - __class_fullname__: albumentations.augmentations.transforms.HueSaturationValue always_apply: false hue_shift_limit: - -10 - 10 p: 1.0 sat_shift_limit: - -10 - 10 val_shift_limit: - -10 - 10
【C#】ソート時のインデックスが欲しい
C#でソートを行い、ソートした時のインデックスも欲しい時に以下で取得できることが分かったのでメモします。
var list = new List<int>() { 3, 2, 1 }; var sorted = list.Select((x, i) => new KeyValuePair<int,int>(x, i)) .OrderBy(x => x.Key); foreach (KeyValuePair<int,int> kv in sorted) { // Keyにソートされた値、Valueに元のインデックスが入ります Console.WriteLine(kv.Key + " " + kv.Value); }
【OpenCV】楕円内部のみぼかしたい
cv2.Blur関数を使って画像をぼかしたいのですが、四角形ではなく円もしくは楕円形状でぼかしたいという状況です。 これはマスクを利用して実現可能なことが分かったのでメモします。
import cv2 import numpy as np def apply_ellipse_blur(image, x, y, haxis, vaxis, angle, ksize): # 全体をぼかす blurred_image = image.copy() blurred_image = cv2.blur(blurred_image, ksize) # 楕円形マスクの作成 maskShape = (image.shape[0], image.shape[1], 1) mask = np.full(maskShape, 0, dtype=np.uint8) cv2.ellipse(mask, ((x, y), (haxis, vaxis), angle), (255), -1) # 楕円形マスク外部を取り出す mask_inv = cv2.bitwise_not(mask) img1_bg = cv2.bitwise_and(image, image, mask=mask_inv) # 楕円形マスク内部を取り出す img2_fg = cv2.bitwise_and(blurred_image, blurred_image, mask=mask) # 合成する return cv2.add(img1_bg, img2_fg) if __name__ == '__main__': image = cv2.imread('pic.jpg') # ぼかす中心座標 x = 100 y = 450 # 楕円横方向径 haxis = 100 # 楕円縦方向径 vaxis = 200 # 楕円傾き angle = 20 # ぼけカーネルサイズ ksize = (15, 15) blurred_image = apply_ellipse_blur(image, x, y, haxis, vaxis, angle, ksize) cv2.imwrite('blurred_pic.png', blurred_image)
【Python】データ拡張手法Mixupの擬似コード
PyTorchのカスタムデータセットにmixupをどう入れ込むかの擬似コードメモです。
# これをDatasetの__get_item__に入れ込めば良い def _apply_mixup(self, image1, label1, idx1, image_size): # mixする画像のインデックスを拾ってくる idx2 = self._get_pair_index(idx1) # 画像の準備 image2 = cv2.imread(self._image_paths[idx2]).astype(np.float32) image2 = cv2.resize(image2, (image_size, image_size)) image2 = normalize(image2) # ラベルの準備(アノテーションファイルは1,0,0,0のように所属するクラスが記されている) label2 = np.loadtxt(self._annotation_paths[idx2], dtype=np.float32, delimiter=',') # 混ぜる割合を決めて r = np.random.beta(self._alpha, self._alpha, 1)[0] # 画像、ラベルを混ぜる(クリップしないと範囲外になることがある) mixed_image = np.clip(r*image1 + (1 - r)*image2, 0, 1) mixed_label = np.clip(r*label1 + (1 - r)*label2, 0, 1) return mixed_image, mixed_label # Datasetの__get_item__のidx以外のindexを取得する def _get_pair_index(self, idx): r = list(range(0, idx)) + list(range(idx+1, len(self._image_paths))) return random.choice(r)
mixupについての説明は以下。 qiita.com