旅行好きなソフトエンジニアの備忘録

プログラミングや技術関連のメモを始めました

【PyTorch】Macro Soft F1 Lossを実装する

マルチラベル問題の評価指標の一つにMacro F1というものがあります。 Macro F1はそのままでは微分できないのでロス関数には適さないのですが、評価指標を微分可能にしてロス関数にしてしまおうという考えもあるようです。

towardsdatascience.com

リンクでは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()

becominghuman.ai

【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);
}

stackoverflow.com

【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)

f:id:ni4muraano:20191110224215j:plain f:id:ni4muraano:20191110224223p:plain

stackoverflow.com

【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