まだタイトルない

アウトプット用です

【未完】PyTorch LightningとWeights & Biasesを使った画像タスクパイプラインを作成し始めた

こんにちは

2021年の夏の連休も2021年に引き続きほぼ予定がなかったので、何らかのコンペに充てる気満々だったのですが、いろんな要因で時間を捧げるコンペはありませんでした。

コンペはしてなかったのですが、PyTorch LightningとWeights & Biases を使った画像タスクのパイプライン作成をしていました。いままではそういった物は持ってなくて最初のサブまでとても時間がかかっていたので、パイプラインをいじって学習して推論して初サブまでが早くなればいいと思っています。

今回は作ったものを載せつつ自分用のメモを書いていこうと思います。間違いや改善点ありましたらご指摘ください。

Config

class CFG:
    debug = True
    exp_name = "ex005_local"
    seed = 29
    # model
    model_name = 'efficientnet_b0'
    img_size = 224
    in_chans = 1
    target_col = 'target' # 目標値のある列名
    target_size = 1
    # optimizer
    optimizer_name = 'RAdam'
    lr = 1e-3
    weight_decay = 1e-5
    amsgrad = False
    # scheduler
    epochs = 10
    scheduler = 'CosineAnnealingLR'
    T_max = 10
    min_lr = 1e-5
    # criterion
    criterion_name = 'CrossEntropyLoss'
    
    # training
    train = True
    inference = False
    n_fold = 5
    trn_fold = [0]
    precision = 16 #[16, 32, 64]
    grad_acc = 1
    # DataLoader
    loader = {
        "train": {
            "batch_size": 128,
            "num_workers": 0,
            "shuffle": True,
            "pin_memory": True,
            "drop_last": True
        },
        "valid": {
            "batch_size": 128,
            "num_workers": 0,
            "shuffle": False,
            "pin_memory": True,
            "drop_last": False
        }
    }
  • wandbにconfigも記録するためにはdict型にする必要があるみたいなのでこの設定の持ち方は検討が必要な気がしています
  • そもそもこの設定の持ち方、dataclassでもなさそうだしなんていう呼び方をするのでしょう・・・?インスタンス作らなくてもCFG.model_nameで値取れますよね

Directory

# 環境によって処理を変えるためのもの
import sys
IN_COLAB = 'google.colab' in sys.modules
IN_KAGGLE = 'kaggle_web_client' in sys.modules
LOCAL = not (IN_KAGGLE or IN_COLAB)

if IN_KAGGLE:
    INPUT_DIR = Path('../input/BirdClef2021')
    OUTPUT_DIR = './'
elif IN_COLAB:
    INPUT_DIR = Path('/content/input/')
    OUTPUT_DIR = f'/content/drive/MyDrive/kaggle/BirdClef2021/{CFG.exp_name}/'
if LOCAL:
    INPUT_DIR = Path("F:/Kaggle/BirdClef2021/data/input/")
    OUTPUT_DIR = f'F:/Kaggle/BirdClef2021/data/output/{CFG.exp_name}/'

TRAIN_DIR = INPUT_DIR / "train"
TEST_DIR = INPUT_DIR / "test"

df_train = pd.read_csv(INPUT_DIR / "train_labels.csv")
df_test = pd.read_csv(INPUT_DIR / "sample_submission.csv")

if not os.path.exists(OUTPUT_DIR):
    os.makedirs(OUTPUT_DIR)

def get_filepath(name, folder=TRAIN_DIR):
    path = os.path.join(folder, name[0], f'{name}.npy')
    return path

df_train['image_path'] = df_train['id'].apply(lambda x: get_filepath(x, TRAIN_DIR))
df_test['image_path'] = df_test['id'].apply(lambda x: get_filepath(x, TEST_DIR))
  • 環境に応じた入力データの場所と出力先の設定をします
    • kaggle,colab,localの判断できるの結構便利だと思ってる
  • データのpath用の列も予め用意しておいて問題ないと思います

transform

def get_transforms(phase: str):
    if phase == 'train':
        return Compose([
            Resize(CFG.img_size, CFG.img_size),
            Transpose(p=0.5),
            HorizontalFlip(p=0.5),
            VerticalFlip(p=0.5),
            ShiftScaleRotate(p=0.5),
            Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225],
            ),
            ToTensorV2(p=1.0),
        ])
    elif phase == 'valid':
        return Compose([
            Resize(CFG.img_size, CFG.img_size),
            Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225],
            ),
            ToTensorV2(p=1.0),
        ])
  • normalizeしてなかったらtrainがuint8のままモデルに突っ込まれてエラー出てたのでここも要改善

dataset

class TrainDataset(Dataset):
    def __init__(self, df, transform=None):
        self.df = df
        self.image_paths = df['image_path'].values
        self.targets = df[CFG.target_col].values
        self.transform = transform
        
    def __len__(self):
        return len(self.df)
    
    def __getitem__(self, idx):
        file_path = self.image_paths[idx]
        # タスクに合わせたロード方法
        #image = np.load(file_path).astype(np.float32)# (6, 273, 256)
        #image = np.vstack(image).transpose((1, 0))# (1638, 256) -> (256, 1638)
        image = cv2.imread(file_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        
        if self.transform:
            image = self.transform(image=image)["image"]
        else:
            image = image[np.newaxis,:,:]
            image = torch.from_numpy(image).float()
        #label = torch.tensor(self.targets[idx]).float()
        label = torch.tensor(self.targets[idx]).long()
        return image, label

LightningDataModule

PyTorch Lightning要素

class DataModule(pl.LightningDataModule):
    
    def __init__(self, train_data, valid_data, test_data):
        super().__init__()
        self.train_data = train_data
        self.valid_data = valid_data
        self.test_data = test_data
        
    # 必ず呼び出される関数
    def setup(self, stage=None):
        self.train_dataset = TrainDataset(self.train_data, transform=get_transforms(phase='train'))
        self.valid_dataset = TrainDataset(self.valid_data, transform=get_transforms(phase='valid'))
        self.test_dataset = TrainDataset(self.test_data, transform=get_transforms(phase='valid'))
        
    # Trainer.fit() 時に呼び出される
    def train_dataloader(self):
        return DataLoader(self.train_dataset,
                          batch_size=CFG.loader["train"]["batch_size"],
                          shuffle=True,
                          num_workers=CFG.loader["train"]["num_workers"],
                          pin_memory=True,
                          drop_last=True)

    # Trainer.fit() 時に呼び出される
    def val_dataloader(self):
        return DataLoader(self.valid_dataset,
                          batch_size=CFG.loader["valid"]["batch_size"],
                          shuffle=False,
                          num_workers=CFG.loader["valid"]["num_workers"],
                          pin_memory=True,
                          drop_last=False)

    def test_dataloader(self):
        return DataLoader(self.test_dataset,
                          batch_size=CFG.loader["valid"]["batch_size"],
                          shuffle=False,
                          num_workers=CFG.loader["valid"]["num_workers"],
                          pin_memory=True,
                          drop_last=False)

LightningModule

これもPyTorch Lightning要素

class Trainer(pl.LightningModule):
    def __init__(self, cfg):
        super().__init__()
        self.cfg = cfg
        self.model = get_model(cfg)
        self.criterion = get_criterion()
    
    def forward(self, x):
        output = self.model(x)
        return output
    
    def training_step(self, batch, batch_idx):
        x, y = batch
        # mixup とかしたい場合はここに差し込む
        output = self.forward(x)
        labels = y
        loss = self.criterion(output, labels)
        try:
            train_score = accuracy_score(labels.detach().cpu(), output.argmax(1).detach().cpu())
            self.log("train_score", train_score, prog_bar=True, logger=True)
            self.log('train_loss', loss)
        except:
            pass
        
        return {"loss": loss, "predictions": output, "labels": labels}
    
    def training_epoch_end(self, outputs):
        preds = []
        labels = []
        
        for output in outputs:
            preds += output['predictions']
            labels += output['labels']

        labels = torch.stack(labels)
        preds = torch.stack(preds)

        train_score = accuracy_score(labels.detach().cpu(), preds.argmax(1).detach().cpu())
        self.log("mean_train_score", train_score, prog_bar=True, logger=True)
        self.log("lr", self.optimizer.param_groups[0]['lr'], prog_bar=True, logger=True)
    
    def validation_step(self, batch, batch_idx):
        x, y = batch
        output = self.forward(x)
        labels = y#.unsqueeze(1)
        loss = self.criterion(output, labels)
        self.log('val_loss', loss, on_step= True, prog_bar=True, logger=True)
        return {"predictions": output, "labels": labels}
    
    def validation_epoch_end(self, outputs):
        preds = []
        labels = []
        
        for output in outputs:
            preds += output['predictions']
            labels += output['labels']

        labels = torch.stack(labels)
        preds = torch.stack(preds)

        val_score = accuracy_score(labels.detach().cpu(), preds.argmax(1).detach().cpu())
        self.log("val_score", val_score, prog_bar=True, logger=True)
        
    def test_step(self, batch, batch_idx):
        x = batch        
        output = self(x).sigmoid()
        return output
    
    def configure_optimizers(self):
        ### optimizer
        #optimizer = get_optimizer(self.parameters(), self.cfg)
        self.optimizer = get_optimizer(self, self.cfg)
        self.scheduler = get_scheduler(self.optimizer)
        return {'optimizer': self.optimizer, 'lr_scheduler': self.scheduler}

ログを記録したい値は、self.log に指定すると自動で記録される。epochは特に指定してないですが記録されちゃいます。

f:id:teyoblog:20210817122457p:plain

  • train_scoreとtrain_lossがstepでlogつけてるのですが明らかに頻度が少ないのでtryからだして様子を見るなど残課題が残っています。

main部分

def train() -> None:
    for fold in range(CFG.n_fold):
        if not fold in CFG.trn_fold:
            continue
        print(f"{'='*38} Fold: {fold} {'='*38}")
        # Logger
        #======================================================
        lr_monitor = LearningRateMonitor(logging_interval='step')
        # 学習済重みを保存するために必要
        loss_checkpoint = ModelCheckpoint(
            dirpath=OUTPUT_DIR,
            filename=f"best_loss_fold{fold}",
            monitor="val_loss",
            save_last=True,
            save_top_k=1,
            save_weights_only=True,
            mode="min",
        )
        auc_checkpoint = ModelCheckpoint(
            dirpath=OUTPUT_DIR,
            filename=f"best_auc_fold{fold}",
            monitor="val_score",
            save_top_k=1,
            save_weights_only=True,
            mode="max",
        )
        
        wandb_logger = WandbLogger(
            project='atma11',
            group= f'{CFG.exp_name}',
            name = f'Fold{fold}',
            save_dir=OUTPUT_DIR
        )
        
        data_module = DataModule(
          df_train[df_train['fold']!=fold],
          df_train[df_train['fold']==fold], 
          df_train[df_train['fold']==fold], 
        )
        data_module.setup()
        #early_stopping_callback = EarlyStopping(monitor='val_auc',mode="max", patience=2)
        
        trainer = pl.Trainer(
            logger=wandb_logger,
            callbacks=[loss_checkpoint, auc_checkpoint, lr_monitor],
            default_root_dir=OUTPUT_DIR,
            gpus=1,
            progress_bar_refresh_rate=1,
            accumulate_grad_batches=CFG.grad_acc,
            max_epochs=CFG.epochs,
            precision=CFG.precision,
            benchmark=False,
            deterministic=True,
        )
        model = Trainer(CFG)
        trainer.fit(model, data_module)
        wandb.finish()
  • 複数fold継続して学習できるようにしてます
  • ModelCheckpoint
    • monitorに指定した値(self.logのname?)で更新されたら保存、最後は保存等指定できる
    • 複数実験で保存するファイル名がかぶっても上書きされないようです
    • 私の環境ではTrainer側のdefault_root_dirでなくModelCheckpoint側のdirpathディレクトリを指定しないとその直下にweightは保存してもらえませんでした。
  • WandbLogger
    • projectはコンペ名で良さそう
    • group名が同じものに関してはまとめられる。グラフの線は平均値、分散が影として描画されます。
    • グラフ同士で比較したい場合はgroupを分ける必要がある。 f:id:teyoblog:20210817122533p:plain

その他

Split

df_train["fold"] = -1
Fold = StratifiedKFold(n_splits=CFG.n_fold, shuffle=True, random_state=CFG.seed)
for n, (train_index, val_index) in enumerate(Fold.split(df_train, df_train[CFG.target_col])):
    df_train.loc[val_index, 'fold'] = int(n)
df_train['fold'] = df_train['fold'].astype(int)
print(df_train.groupby(['fold', CFG.target_col]).size())

Get関数

# ====================================================
# model
# ====================================================
class CustomModel(nn.Module):
    def __init__(self, cfg, pretrained=False):
        super().__init__()
        self.cfg = cfg
        self.model = timm.create_model(model_name=self.cfg.model_name,
                                       pretrained=pretrained,
                                       in_chans=self.cfg.in_chans,
                                       num_classes=self.cfg.target_size)
        
    def forward(self, x):
        output = self.model(x)
        return output
    
def get_model(cfg):
    model = CustomModel(cfg, pretrained=cfg.pretrained)
    # plだと要らない?
    # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    # model.to(device)
    # model.eval()
    return model

# ====================================================
# criterion
# ====================================================
def get_criterion():
    if CFG.criterion_name == 'BCEWithLogitsLoss':
        # plだとto(device)いらない
        criterion = nn.BCEWithLogitsLoss(reduction="mean")
    if CFG.criterion_name == 'CrossEntropyLoss':
        criterion = nn.CrossEntropyLoss()
    else:
        raise NotImplementedError
    return criterion
# ====================================================
# optimizer
# ====================================================
def get_optimizer(model: nn.Module, config: dict):
    """
    input:
    model:model
    config:optimizer_nameやlrが入ったものを渡す
    
    output:optimizer
    """
    optimizer_name = config.optimizer_name
    if 'Adam' == optimizer_name:
        return Adam(model.parameters(),
                    lr=config.lr,
                    weight_decay=config.weight_decay,
                    amsgrad=config.amsgrad)
    elif 'RAdam' == optimizer_name:
        return optim.RAdam(model.parameters(),
                           lr=config.lr,
                           weight_decay=config.weight_decay)
    else:
        raise NotImplementedError

# ====================================================
# scheduler
# ====================================================
def get_scheduler(optimizer):
    if CFG.scheduler=='ReduceLROnPlateau':
        """
        factor : 学習率の減衰率
        patience : 何ステップ向上しなければ減衰するかの値
        eps : nanとかInf回避用の微小数
        """
        scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=CFG.factor, patience=CFG.patience, verbose=True, eps=CFG.eps)
    elif CFG.scheduler=='CosineAnnealingLR':
        """
        T_max : 1 半周期のステップサイズ
        eta_min : 最小学習率(極小値)
        """
        scheduler = CosineAnnealingLR(optimizer, T_max=CFG.T_max, eta_min=CFG.min_lr, last_epoch=-1)
    elif CFG.scheduler=='CosineAnnealingWarmRestarts':
        """
        T_0 : 初期の繰りかえし回数
        T_mult : サイクルのスケール倍率
        """
        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=CFG.T_0, T_mult=1, eta_min=CFG.min_lr, last_epoch=-1)
    else:
        raise NotImplementedError
    return scheduler
  • timm.create_modelnum_classes指定しない例が多い気がしますがどういうことを考慮してるのか教えてもらいたいです。

参考にしたサイト

pytorch-lightning.readthedocs.io

qiita.com

⚡️Pytorch Lightning + Grid Mask + Ranger Opt + W&B | Kaggle

あとがき

とりあえず連休中PyTorch Lightningと戯れたことをアウトプットしてみましたが結構残課題があったり未確認な点も多かったりと中途半端なものになってしまいました。きれいなものを作ることが目的ではないですが、今後はコンペの実践を通してブラッシュアップして行きたいと思います。

今までのコンペはいわゆるピュアpythonで取り組んでましたが、ループのコード、ログの設定、モデル保存の条件式等学習の本質的なコードと同量くらいの機能的なコードがありましたがその部分がライブラリの昨日に統一されてきれいにもなり、フォーマットを気にすることもなくなり良かったと思ってます。今後は今回載せたパイプラインの改善だけでなく他のタスク向けのものも作ったりしながらコーディング技術と機械学習への理解を深めていきたいです。

これにて私の連休は終了です!

【Kaggle挑戦記】MLB コンペ振り返り【#10】

こんにちは。

2021/6/10-2021/7/31 の期間で開催されていたMLB Player Digital Engagement Forecastingというコンペに参加してきました。取組期間は6/29-8/1の約一ヶ月です。結果は2021/9/15に決まるのでまだどうなったかはわかりません。
今回はコンペ中に気になったこととそれに対してやったこと(殆どは何もわからんで対策できず)について書きます。

また、他の参加者がどう考えてたのか、本質は何だったのか聞く機会があればいいなーと思っています。

コンペページ

MLB Player Digital Engagement Forecasting

概要

選手のパフォーマンスデータ、ソーシャルメディアのデータ、市場規模などのチーム要因などを使って、将来ファンがMLB選手のデジタルコンテンツにどのように関わっているかを予測する。

具体的には2021年のシーズンの各MLB選手について、4つの異なるエンゲージメント指標(target1~target4)を予測する。

評価値

  • MCMAE(Mean Column-Wise Mean Absolute Error)
  • 4つのターゲット変数ごとに平均絶対誤差が計算され、その4つのMAE値の平均値がスコアとなる

データ

詳しくはコンペのDataを見るに限りますが選手やチーム等のマスターとtrain.csvが与えられていました。
train.csvが一番メインのテーブルで1行が1日分のデータになっていて、ラベルを始め選手個人のデータやチーム単位のデータ、試合の1球ごとに発生したイベント情報まで含まれています。 与えられてる項目が既に多いためどの特徴量を使うか/使わないか ダブルヘッダーの扱いやnanの扱いでかなり苦労しました。これは技術不足です。

分析

大きく分けると下記です。

  1. ラベルのスケールと他選手との相対的なターゲットに同対応したら良かったの?
  2. レギュラーシーズンのデータが少なくない?
  3. NaNが多かった
  4. 情報のリークによるエンゲージメント増加は流石に予想できないよね

ラベルの値はスケールされてる

f:id:teyoblog:20210805081233p:plain 引用元

  • 各targetは0から100の範囲でラベル付けされている
  • 分布をみるとほとんどが10以下
  • 1日あたりtarget1~4で1つづつ100になってるデータが有る(たまに複数の例も確認)
    • target毎にスケーリングされているのではないかという話がディスカッションで議論されていた
  • このことから、試合がないオフシーズンでも誰かは100になる
  • 普段大谷選手がホームランを打てば100になっていたとしても同日にシャーザー投手の移籍が話題に上がればシャーザー投手が100をつけることになるかもしれない
    • 同日内で一番大きな出来事を見つけられればいいですが私はチャレンジできませんでした。

レギュラーシーズンのデータが少ない

  • trainデータの期間は2018/01/01~2021/04/30
    • コンペ終盤に5月1日から7月17日までのデータが配布される
    • 締切後に7/18~7/31のデータも使えるようになる
      • 学習から推論までするノートを提出することで、rerunしたときに7/31まで使用して学習する
  • public testは2021年5月
  • privateは2021年8月以降の一定期間

test期間はいわゆるレギュラーシーズンの期間にあたります。データの中でレギュラーシーズンは下記

  • 2018年3/29~9/30 162日間2430試合
  • 2019年3/28~9/29 162日間2430試合
  • 2020年7/23~9/27 60日間900試合
  • 2021年4/1~10/3(予定) 162試合(予定)
    • trainには379試合分含まれる

不安点としては下記がありました。みなさんはどう判断していたのでしょうか?

  • 今シーズンのデータが1ヶ月分しかない
    • cvを4月で測る流れになってたが今年のデータを学習に使えない
  • privateはシーズン終盤
  • 直近のシーズン2020年シーズンはコロナ禍で特殊なシーズンだった

色々試せばいいが正義だとは思いますがなかなか実験を早く回すこともできなくて最終的には7/17までのデータが配布されたあとの6/1-7/17をvalidにしました。(privateでは31までがvalidになる)直近の2ヶ月分をvalidだけで使うのはもったいなすぎるためbestiteration*1.1で再学習をするようにしました。

target毎に若干相関がある?

player_engagement_targets_correlations = df_targets[['target1', 'target2', 'target3', 'target4']].corr()
display(player_engagement_targets_correlations.round(decimals = 4))

f:id:teyoblog:20210805081254p:plain

何らかのメディアのエンゲージメント4種なのでむしろ低いのではないか?という捉え方も出来ますが、、、
lightgbmだと4つそれぞれにモデルを組むため他のtargetとの関係は考慮されません。そのためスタッキングを試してみましたが、用意したモデルが少なかっただけなのかうまくいきませんでした。

野手と投手でデータがある列が違う

  • 野手だと投手用特徴量はNaNになっている。
  • DH制の投手は打撃成績はNaNになっている。

打者用のモデル、投手用のモデルを考えましたがやりませんでした。

サイクルヒットを達成しても100になるとは限らない

サイクル安打ってすごいし話題にもなるので特徴になるかと思いましたが意外と100にならない場合がありました。

player_score.query('doubles > 0 & triples > 0 & homeRuns > 0 & hits >= 4 \
                    & hits - doubles - triples - homeRuns > 0 ').reset_index(drop=True).merge(targets, on=['dailyDataDate','playerId'], how='left')

f:id:teyoblog:20210805081319p:plain

サイクル安打が達成された日に、その人以外が100をとった要因を調べてみました。

  • 2018-08-09 Giancarlo Stanton 2015年に導入されたスタットキャスト史上最速となる121.7mph(約196km/h)の本塁打を放った
  • 2018-08-09 Kenley Jansen 宿泊ホテルで不整脈を訴え、そのまま病院に搬送され故障者リスト入り
  • 2018-08-29 不明 31のヤンキース移籍のときには3つ100がついた 噂でも出ていた?
  • 2018-09-17 Aaron Judge 怪我からの復帰戦
  • 2018-09-30 Christian YelichがナショナルリーグMVPに選ばれる
  • 2018-09-30 josh hader 最優秀救援投手になったから?
  • 2018-10-08 ポストシーズンヤンキースに登板予定?登板して好投?
  • 2019-04-05 エンゼルス mike trout 2ホームラン
  • 2019-04-05 Bryce Harper シーズン開幕して、調子がいい()
  • 2019-06-13 Greinke 7かい無失点好投
  • 2019-06-14 t4 Joe Mauer メジャーにいない・・・
  • 2019-06-14 edwin Encarnación ヤンキースに移籍 (発表は6/19頃)もれていた・・・?
  • 2019-07-23 ヤンキースが劇的勝利、ヒックスとグレゴリウスがホームラウンを打っている
  • 2019-07-23 マイク・トラウト33号ホームラン
  • 2019-08-05ブルワーズイエリッチ2HR
  • 2019-08-05マーリンズイーサン・ディアスメジャー初ホームラン
  • 2019-08-05 アレクマノアはプロ入り前
  • 2019-09-17 mik yastrzemski のチームが延長15回で勝利、 当人はホームランを打ったが殊勲ではない
  • 2019-09-17 ヤンキースの投手 投げてない・・・

なんとなく、ホームランを打つと盛り上がる人がいたりヤンキースは盛り上がる・・・?

特徴量としてその日大谷はホームランを打ったか?トラウトはホームランを打ったか?という特徴量を作ってみました。 殆ど効かなかったです。

eventデータを使えば終盤に逆転勝ちをしたかどうかや、殊勲打打った人とかはわかりそうだと思いましたが力が足りず手を付けられませんでした。

edwin Encarnación選手のヤンキースに移籍によるエンゲージメントの増加はwikiやネットニュースで書かれてる公式発表から数日前にすでに100になっているので何らかのリークや噂が立ってるのかもしれないですが与えられたデータからは判断できません。transaction内には含まれてるので発表当日の高い値は予測できるかもしれませんが、移籍全てが高くなるわけでもないので他の特徴量との組み合わせになるでしょう。

その他特徴量

実験管理を雑にしすぎててどれくらい効いたのかあまり残ってないですが...

  • チームの成績、完封勝ちしたか、完封負けしたか
    • 0.01
  • アメリカ人かどうか
    • +0.01
  • 何個賞を受け取ったか
  • トレードの発表があったか
  • 打点はその日のチームの総得点の何割か
  • 人気選手がホームランを打ったか(Ohtani,Trout,Judge,Yelich,Acuna.jr
  • 年俸データ
  • 記述統計量
    • +0.06
    • なるべく直近のターゲットで集計しました f:id:teyoblog:20210805081330p:plain

提出コード

www.kaggle.com

試したけど採用されなかったもの

  • lightgbmのobject hover
  • lightgbmでweightを設定
  • 個人、チームのツイッターアカウントのフォロワー数
  • スタッキング
  • NNmodel

おわりに

今回はまだ評価期間なこともあって、忘れないようにコンペ中気になったことを雑に書き並べてみました。(サイクルヒット打っても100にならない!を伝えたかっただけかもしれない。というか期間中にディスカッションに掛けばよかったとまで思います。

前回のコンペ(Coleridge)が終わってから1週間で次のコンペを始められたのは良かったと思います。が、もう少し序盤からペースよくすすめられたら良かった。まだまだ実験の速度が遅いと痛感しました。

【Kaggle挑戦記】Coleridge Initiative コンペ振り返り【#9】

※このページにはColeridgeコンペで良スコアを出す方法は書かれていません。初めて自然言語タスクに挑戦したのでその記録です。

概要

科学論文(英文)の中から、使用しているデータセットについて言及している箇所を抽出してくるというのが本コンペのタスク

  • 論文内でTypoがあればTypoをそのまま抽出する
  • 評価値はJaccard係数ベースのFBetaスコアで評価
  • clean_textを通して文字列の規格をそろえて評価させる

    python def clean_text(txt): return re.sub('[^A-Za-z0-9]+', ' ', str(txt).lower())

  • train labelからマッチングで抽出してもいいprivateスコアにはならない

DiscussionやCodeから推察するにNLPの中でもNamed entity recognition (NER)タスクに当たる課題のようだが、お作法が全くわからないので周辺知識から1つづつ学んで最終的になにかサブミットをすることを目標に取り組む。

評価値

  • F0.5

    • 分母のprecisionに0.25が乗算されるのが特徴(recallは等倍) f:id:teyoblog:20210624122839p:plain
  • FPのペナルティがが大きい

  • Jaccard距離0.5以上の予測ラベルをTPにする
  • Jaccard距離0.5以上だが、すでに他の予測ラベルと対になっているGroundTruthの場合その予測値はFP
  • GroundTruthになかったらFP
  • 予測できなかったらFN

つまり、予測し過ぎは大きいペナルティを受けやすい?

ポイント

本コンペのポイントの一つとしてラベルが挙げられます。trainラベルはpublicに使われるデータには含まれるが、privateには含まれないということがホストより明示されていました。

ホストが求めるソリューションは文字列照合ではなく統計的、機械学習的なソリューションで汎用的に利用できるものですが上記ことによりtrainラベルを使ったliteral matchingをするとpublicLBではスコアが高く見えるという事象が発生してたように思います。

参考

A percentage of the public test set publications are drawn from the training set

publicテストセットの出版物の一部は、トレーニングセットから抽出されています。(文字列マッチングで正解できるデータがpublicにある)

  • testdata数が8000個(public960(traindataも一部あり),private7040)

基本的にBERTを使う雰囲気

NLPについてゼロから学びつつコンペにも取り組んでいく。まず公開ノートやディスカッションを眺めるとspaCyやBERTが使われていることがわかる。いい機会なのでBERTの使い方を本コンペでは学んでいくことにしました。

【解説記事】BERT解説:自然言語処理のための最先端言語モデル

  • 記事によるとコンペ内で見られるMLMという単語はMasked Language Modelの略称でBERTの事前学習戦略の2つの内の1つがこのMLM
  • もう一つはNSP(Next Sentence Prediction)という次文予測

MLM

f:id:teyoblog:20210624122943p:plain

GELU(Gaussian Error Linear Units):活性化関数

BERTモデルの学習においては、マスクされたLMと次文予測というふたつの戦略に関する損失関数の結合が最小化するというゴールにむかって、このふたつの戦略がいっしょに学習される。

【PyTorch】BERTの使い方 - 日本語pre-trained modelsをfine tuningして分類問題を解く

FineTuningすることでセンチメント分析、QA、固有値解析(NER)等様々なタスクに適用可能

本コンペの情報ではQAとNERの文字を見かけるのでまずはNERについて見ていく。

BERTを使用する

BERTを使用するにはHugging Face の Transformersライブラリがデファクトスタンダードな様子。BERTなどの最先端のアルゴリズムを簡単に試すことができる。pytorch,tf対応。他のライブラリがあるのかまでは未調査。

学習済みモデルを使って推測する

pipeline()を使うことでかんたんに各種タスクを実行できる

【参考サイト】【NLP】Hugging Faceの🤗Transformersことはじめ

finetuningして使う

コンペなどでしっかり精度を出していくにはタスクで与えられたデータを使ってfinetuningしていくことが必要になる。

学習方法は下記のような選択肢があるっぽい

  • transformersリポジトリにあるrun_ner.py (ner部分はタスクによる)スクリプトを使う
  • 普通にpytorch ライクにdatasetとdataloderを宣言してつかう

MLM

学習note [Coleridge] BERT - Masked Dataset Modeling

予測note [Coleridge] Predict with Masked Dataset Modeling

  • ほとんどのデータセット名は、頭文字が大文字の単語と、on, in, and などのストップワードで構成されている
  • 大文字で書かれた単語をすべて探し、dataset名を表しているか、そうでないかを$と#でラベル付けし、MLMタスクで学習をする

NER

【Techの道も一歩から】第26回「BERTで日本語固有表現抽出器を作ってみた」

上記事のリンクが404になっているが、リポジトリのtoken-classificationのところにあるっぽい。学習したいテキストデータをBERTように整形(前処理)、label付けをしてからrun_ner.pyを叩いて学習をするということがわかる。

一連の流れがわかるとなんとなく公開ノートで行われてることもわかってくる。与えられてるのが、論文の文章とデータセット名を言及している部分(抜粋)なので、この情報をもとにlabeling(タグ付け)作業が必要になってくる。

タグについては【Techの道も一歩から】第34回「固有表現抽出のためのデータを作る」 R&D 連載にIOB2タグについて載っている。引用すると

  • B: 固有表現の先頭
  • I: 2トークン以上で構成される固有表現の先頭以外のトーク
  • O: 固有表現以外のトーク

学習を始めると吐き出されるWarning

  • You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.(このモデルを予測や推論に使えるようにするには、ダウンストリームのタスクでトレーニングする必要があるでしょう。)

結果

公開ノートを参考にしながら複数モデルを組み合わせました。各モデルの予測をunionして、jaccardスコアをつかって重複してそうなラベルは削除しました。結果は散々ですが複数モデルの組み合わせで若干スコアはあげられてたみたいです笑

f:id:teyoblog:20210624123027p:plain f:id:teyoblog:20210624123034p:plain f:id:teyoblog:20210624123041p:plain

また、物は使いようではありますが、1stの意見ではBERT型のモデルを考えなしにつかうとtrain label(privateにはないと言われてる)にoverfittingするという話が印象的でした。 Coleridge Initiative - Show US the Data | Kaggle

The reason why GPT worked and Roberta failed so hard is that Roberta is too clever for its own good. When you try to build a text extraction model with a single query of finding dataset names, Roberta doesn't need to care about the context and will try to find a substring that most resemble a dataset name, thus lead to overfitting.

最後に

winner's solutionをちょこちょこと読んでますがわからない部分が多く、solutionで勉強になればと思ってたもののなかなか厳しそうです。
また次のコンペがんばります。

おまけ

今回はNotionでコンペの取り組みを管理してみました f:id:teyoblog:20210624124314p:plain

本記事のドラフトはコチラです。 www.notion.so

【Kaggle挑戦記】鳥コンペ2(BirdCLEF 2021)銅メダル振り返り【#8】

f:id:teyoblog:20210605153916p:plain:w500

こんにちは

先日終了した鳥コンペ2に参加してきました。

結果はpublic41位からprivate66位で目標としていた銀メダル以上を取ることはできませんでした。

本コンペではTakamichi Todaさんと、shinmura0さんのチームに途中から仲間にしてもらいチームでの取り組みとなりました。この記事では自分がやってたCNN部分の取組内容を書いていきます。

自分がCNNで進めていた理由は過去の鳥コンペと、鳥蛙コンペでの上位solutionで使われており、十分戦えると判断したためです。SEDについて全然わからないのでもっと積極的に教えてもらいに行けばよかったと後悔している・・・。

コンペ概要

与えられた音声データの中で、指定された鳥のどの種類の鳥が鳴いているのか、どれも鳴いていないか(=nocall)を判別するマルチラベル分類

似たようなコンペがkaggleで過去にあったため今回は鳥コンペ2といえる。

前回のコンペとの違い

  • 対象となる鳥の種類が増えた。
    • 識別する鳥の種類が264種類から397種類に増えました
  • 音声ファイルのメタ情報が増えた。
    • いつどこで録音されたものかの情報が増えました。
    • 例えば日本で録音されたデータには日本にいない鳥の鳴き声は入り得ないので、後処理でご識別を削除という対応が取れます。
  • テストデータでCVを算出できるようになった。

過去コンペ

データ

traindata

  • train_short_audio
    • 397種の鳥の音源を合計62874ファイル
    • フォーマットはogg
    • サンプリングレートは(たしか)事前に32000にサンプリングし直し済み
    • 音源中で鳴いてるわけではない
  • train_soundscapes
    • testdata(test_soundscapes)とほとんど同じデータ
    • 録音場所や、時期、機材がほとんど一緒とされている

oggファイルについて

oggファイルのまま学習すると(特にkaggle環境では)ファイルの読み込みがボトルネックになって非常に時間がかかる状態でした。wavに変換したり、画像に変換済みのデータセットが公開されていたのでこれを使うなどの対応がありました。

音源全体でその鳥が鳴いてるわけではない

課題点として、音源データを通してすべての時間帯で鳥が鳴いているわけではないし、どこで鳴いてるかのメタ情報がない事が挙げられます。secondary labelsも与えられてますがこちらはさらに頻度は少ないと考えられます。下の図のようなイメージです。

f:id:teyoblog:20210605154542p:plain

testdata

10分の音源があり、5秒おきに397種のうち鳴いてる鳥があればそのラベル(複数可),鳴いていなければnocallをつけます。

f:id:teyoblog:20210605154552p:plain

データの差

  • レーニングデータと公開されてないテストデータとの間にはドメインシフトがある
    • train_short_audioはxeno cantoに登録されているもの
    • soundscapesは実際の鳥の声(うまく言えない
    • 録音機材機材などが違ってくる
  • soundscpapeではnocallというラベルが存在する
  • soundscapesでは複数のラベルをつけることもある

取組内容

パイプラインは大まかに下記の流れです。

  1. データの準備(音声から5秒間分切り抜いてくる)
  2. Data augmentation(waveform transform)
  3. logmelspec(画像)に変換
  4. Data augmentation(spec augmentation)
  5. Training
  6. postprocess

1. データの準備

traindataの箇所でも触れましたが、音源からランダムで5秒間切り取ってきてこれはスズメの鳴き声です!とlabelつけして扱うと、環境音しか入ってないけどスズメとして学習がすすみます。そのため、如何にprimaly labelの鳥の声が入ってる部分を切り取るかというのはまず取り組まなければいけないことと判断しました。

これは鳥1でも同じですので、過去コンペSolutionを見てといくつか対応策を候補に上げました。

  • ハンドラベリングする
    • 一番強そう
  • out-of-foldで正解予測確率が高い部分をクリップ
  • RMSトリミングを使って鳥の鳴き声を含むクリップを取得
  • 最初の5秒か、最後の5秒に鳥が含まれていると仮定する
  • 5秒でなくもっと長くとってくる
    • train時の画像が時間方向に長くなるが画像内の特徴を学習するから問題ない選択肢と判断していましたが認識が違えば指摘ください(ちなみにこれは試しましたがうまくいきませんでした。)

今回はチームメンバーが一部ハンドラベリングしてくれていたデータが有ったことと、最後5秒は人の声とかが入ってる場合があるとのことで、基本最初の5秒+ハンドラベリングされてるファイルならそこが入るように切り抜くという方法を取りました。

効果としてはLB0.04くらい上がりました。

また、鳥1と違って今回は音声データ(1dデータ)をGPUに乗せてから、torchlibrosaを使ってスペクトラム変換、ログメル変換、spec augment を行いました。

2.Data augmentation

  • NoiseInjection
  • PinkNoise
  • RandomVolume
  • 環境音(ESC50)
  • modify mixup

modify mixupは鳥1の3rd place solutionで紹介されていた手法で、同一バッチ内で組み合わせて、ラベルはunionを取るものです。

環境音に関してはあまり改善が見られなかったのですが、最終サブでは使用されました。
SSWの録音してる場所の近くには空港がある等様々な自然音の情報もありましたので、入れて学習することは理にかなって入ると思います。

3.logmelspec(画像)に変換

torchlibrosaを使ってもModel内で音声データ⇛スペクトログラム⇛ログメルスペクトログラムへと変換していきます。パラメータは下記

  • sample_rate = 32000
  • n_mels = 128
  • fmin = 20
  • fmax = 16000
  • n_fft = 2048
  • hop_length = n_fft//4

ここからチャンネル数を3chにします(imagenetでのpretrained weightが3chのため) 今回は鳥1の4-th place solutionを参考に delta,deltadeltaを使うことにしました。

他にも単純にrepeatするだけであったり、repeatしてからimagenetに合わせた標準化をしたり、こちらのスライドでは(melspec,pcen,melspec**1.5)というのも紹介されています。

4.Data Augmentation

こちらでもtorchlibrosaを使ってSpecAugmentationを行います。前回のコンペでも聞いていたため最初から使っていました。

結局解決してないというか考えるのをやめたのですが、モデル内で音声からログメルまで変換したり、SpecAugmentationをした場合、逆伝播はどうなってるんでしょうね・・・

5.training

StratifiedKFoldで5-foldに分割し、学習。

val loss best よりもTrainSoundScapeに対するスコアが高いとき方がpublicLBがよく、またTrainsoundscapeと LBは比較的相関が取れてたのでよかったです。val loss bestに比べて0.01-0.02ほどスコアが良くなったと思います。

f:id:teyoblog:20210605154644p:plain

推移を見てみると、lossに比べてTrainsoundscapeに対する指標が暴れてるので、これをいい感じにできる学習を目指すのがいいのかなと思いましたが結局できませんでした。

6.postprocess

  • 前回コンペのソリューションからpredictionの平滑化(前後の時間のpredictionを使う)を採用したところスコアは向上
  • 鳥2では収録場所がアメリカ(カリフォルニア、ニューヨーク)、コスタリカ、コロンビアト決まっていて、ファイル毎にどこでいつ収録されたのかもわかるため、その時期そこにいない鳥という情報を使った後処理も実施
  • 閾値最適化
    • 今回のソリューションで一番好きな部分です。こういうのを思いついて実装できるようになっていきたい。

下記gitから引用

閾値の決め方は

  • TSにおいて、model_predictを取得
  • このとき、labelがnocallではなく、「各鳥でnocall」の部分を取得する
  • このnocall_predictは、各鳥で結構な時間(2時間以上)集まるので信頼性がある
  • 次に、各鳥でnocall_predictの分布を指数分布にあてはめ、閾値を決める
  • 閾値は0.2 + 99.9%(指数分布)が標準だが、モデルによって調整する

f:id:teyoblog:20210605154654p:plain

from scipy.stats import expon

def get_threshold(score, label): #score(2400,397)  #label(2400,397) is 1 or 0
    optim_thresh = np.zeros(397)
    
    for i in range(397):
        target_score = score[:, i]
        target_label = label[:, i]
        nocall_score = target_score[target_label==0]

        # 指数分布にフィッティング
        fit_parameter = expon.fit(nocall_score)
        frozen_expon = expon.freeze(*fit_parameter)

        # 0.2 + 99.9%で閾値を決める
        optim_thresh[i] = frozen_expon.ppf(0.999) + 0.2
            
    return optim_thresh # shape(397)

他にも提出したもう一つのサブでは重み付きvotingなどでスコアが上がっていました。

この後処理に関しては私はほとんど携われていないので反省の限りです。

試したけど駄目だったもの

  • high contrastにする
  • GaussianNoise
  • 学習時の音声データの長さを長くする
  • labelsmooting
  • 最後に2層drop out
  • site別にfinetuning
    • そのsiteにいない鳥をnocallとして入れる
  • efficientnet
  • ViT系
  • melspecを224まであげる

結果/感想

結果

冒頭に書いたとおりShake downして銅メダルに終わりました。ちょっとpublicにover fitしていたかもしれませんが、結果を見るとTrainsoundscape,public,privateすべて連動している感じだったので、過剰なoverfitではなかったのかもしれません。上位solutionも出揃ってきたので、引き続き勉強して次のコンペに挑みたいと思います。

感想

今回は既存のチームに入れて貰う形でチームをくんで2-3週間取り組ませてもらいました。改めて二人にはくんでいただいて感謝です。個人的にはチームをくんだ結果メンバーに甘えてだらけてしまうこともなく、ソロでやってるよりも真剣にできたのではないかと思います。コンペ中に気になったことを雑談できるだけでもスッキリしますし、今回の結果は私のモデルがベースになってるものの一人では到達できなかった地点なのでチームは学びにも結果にもいいものがあると改めて感じました。

取り組みのペース?として実験数や、アイデアを実装して実験する速度の速さを目の当たりにしたのでもっと技術的なレベルアップもしていきたいと強く感じました。

今回はcolacproを活用するということで、kaggleでコーディングしてローカルとcolabでノートブックを走らせるということをしました。そこで困ったこととして、例えば Dataset のコードを更新したときに手元で枝分かれしたコードたちからForkするときにDatasetのコードが過去のものになっていたりと、コードの管理がうまくできませんでした。また、colabproではバッチ数をローカルより大きくできるのですが、したときの学習率が悪かったのかうまく学習が進まないことが多発してしまい苦労しました。

solution

最後に、CV/public/privateベストになったsolutionを図にしてみました。

f:id:teyoblog:20210605154704p:plain

Links

ヨーロッパ旅行記⑦[プラハ観光]

はじめに

この旅行記は昔したヨーロッパ旅行の記録です。

1週間ほどかけてドイツ、チェコオーストリアを周遊しました。


できる限り自分が見たい旅行記に近づけるようにがんばって書いています。
ここが良い、これがほしいありましたらご意見待ってます。

バックナンバー

ヨーロッパ旅行記①[出発~フランクフルト]

ヨーロッパ旅行記②[フランクフルト~ローテンブルク~フュッセン]

ヨーロッパ旅行記③[ノイシュバンシュタイン城~フュッセン市街地]

ヨーロッパ旅行記④[ミュンヘン]

ヨーロッパ旅行記⑤[チェスキークロムロフ]

ヨーロッパ旅行記⑥[プラハ]

今回の旅程

今回は5日目、チェコプラハ観光です。

f:id:teyoblog:20210410222002j:plain

宿泊地のアパルトメントから、旧市街を寄って、カレル橋を渡り、プラハ城まで行きます。

まずは荷物を預ける

宿泊先からプラハ本駅に行き荷物を預けます。

移動が多い海外旅行のポイントはやはり荷物を預けることですね。

f:id:teyoblog:20210326202237j:plain

f:id:teyoblog:20210326202312j:plain

駅のホール?ドームが壮大で印象的でした。

徒歩で旧市街まで

f:id:teyoblog:20210326203019j:plain

f:id:teyoblog:20210326203000j:plain

それっぽい町並みを歩いていきます

f:id:teyoblog:20210326203113j:plain

f:id:teyoblog:20210326203133j:plain

路面が変わって、朝市のようなものがやっている通りもありました。
今思えば立ち寄ればよかった

f:id:teyoblog:20210326203952j:plain

旧市街広場につきました。

f:id:teyoblog:20210326204040j:plain

朝早くで人通りも少ないので、結婚の前撮りとかでしょうか?ウエディングドレスで撮影している姿も

 

ひとまず、プラントしてはまずプラハ城で後でまた来るので黙々と進みます。

f:id:teyoblog:20210326204207j:plain

Y字路

 

f:id:teyoblog:20210326204340j:plain

この路地映画ミッション・インポッシブルで見た気がするんですけど、わかる人いますか?

 カレル橋を通って

f:id:teyoblog:20210326204429j:plain

カレル橋を通ってヴルタヴァ川をプラハ城側に渡ります。
見ても良い景観ですし渡っても賑わい豊かで楽しかったです。

f:id:teyoblog:20210327203122j:plain

見どころの橋塔

f:id:teyoblog:20210327203253j:plain

見どころの聖人像。左右で合計30体みたいです。

一個上の橋棟の向こうに写ってますがその存在感は伝わるでしょうか?

黒飛び失礼・・・

f:id:teyoblog:20210327204126j:plain

橋から見えるプラハ

f:id:teyoblog:20210327204346j:plain

橋を渡ると居住地だからか公園もありました。

f:id:teyoblog:20210327204427j:plain

これは陸地なのですが橋塔のひとつの股みたいです

f:id:teyoblog:20210327204444j:plain

朝ごはんはマクドナルドです。
 大事な位置情報も張っておきますね

マクドナルドから15分ほど坂道を登ってプラハ城に向かいます

f:id:teyoblog:20210327205847j:plain

トラムもいい味

f:id:teyoblog:20210327205304j:plain

f:id:teyoblog:20210327205324j:plain

f:id:teyoblog:20210327205342j:plain

振り返ると景色が良くなってきます。

プラハ城到着

開けたフラッチャ二広場があります。

f:id:teyoblog:20210327210121j:plain

 城内マップ

プラハ城地図下記サイトから引用

プラハ城マップ - プラハ城 ツーリスト・サイト

ボディチェックを受けて入場すると聖ヴィート大聖堂があります

f:id:teyoblog:20210327221936j:plain

なんとかてっぺんまで映る壮大さ。

装飾もかっこいい

f:id:teyoblog:20210327222055j:plain

人と比べるとサイズ感伝わりますかね

f:id:teyoblog:20210327222154j:plain

観光客はたくさんいますが大聖堂ということで静かな空間というのが特別感を感じます。

f:id:teyoblog:20210327222350j:plain

f:id:teyoblog:20210327222434j:plain

f:id:teyoblog:20210327222453j:plain

ステンドグラスをもっといい感じに撮れるようになりたい

 

反対側から見上げてみます

f:id:teyoblog:20210327222908j:plain

f:id:teyoblog:20210327222929j:plain

大きさ伝わる・・・?

f:id:teyoblog:20210327223437j:plain

登ります!!!

 高さは約100m、280段ぐるぐるぐるぐると...

f:id:teyoblog:20210404223237j:plain

写真の順番的にのぼったとこにあったはずだけどあまり記憶にないです。

f:id:teyoblog:20210404223512j:plain

景色、一番左がカレル橋、奥にも同じ様な脚した橋が何個か見えます。

f:id:teyoblog:20210404223727j:plain

ドアップ。像も視認できます。

オレンジ?の屋根にたまに緑、壁もパステルよりのカラフルできれい。

f:id:teyoblog:20210404224004j:plain

鶏?

f:id:teyoblog:20210404224113j:plain

大聖堂の塔、ゴツい

黄金の小道

f:id:teyoblog:20210404225126j:plain

城内の黄金の小道に行きます。最近読んだ『世界の路地』という本で紹介されてたので、比較的有名なスポットなのでしょうか?

f:id:teyoblog:20210404225601j:plain

確かに小道

f:id:teyoblog:20210404225721j:plain

ぼかしていくー

f:id:teyoblog:20210404230029j:plain

 

f:id:teyoblog:20210404225751j:plain

背景ぼかしていくー

f:id:teyoblog:20210404230128j:plain

写真はしょうもないものですが、寒かったのでホットワインで温まります。

 

気合い入れて写真を貼りすぎたので今回はここまでにして、次回はプラハ城を出てから始めることにします。

 

それでは

【Kaggle挑戦記】バスケコンペでメダル拾えるかと思ったらそんな甘い話なかった【#7】

こんにちは!役立たずてよです!
自分で役立たずとは言いつつも、「まぁそこまででもないっしょ!」と思っていました。 でも、今回のバスケコンペで...................

こんにちは、kaggleのバスケコンペに参加してきました。

NCAAはNational Collegiate Athletic Associationの略で全米大学体育協会という意味です。そのなかのバスケットボール男子/女子の大会の結果を予測するというコンペです。

毎年有るイベントのためバスケコンペは近年は毎年の定番になっているみたいで、去年は情勢的に中止になってしまったものの2019や2018のソリューションを参考にすることができました。 また、大きな違いとして今年からメダル対象になっています。下表のようなチーム数がメダルを手にすることが出来ます。運要素が強いとはいえ他のコンペと比べたら銀メダル狙いやすそう?

下記は参加チーム数とメダルのボーダーになります

参加チーム数 金圏 銀圏 銅圏
707 11 50 100
451 10 50 100

今回は過去ソリューションを参考にサブを作ったのでオリジナル要素をアウトプットすることは出来ないです。そのかわり、来年の自分やこれから始める人の学びになりそうなことを書いていこうと思います。

サブミット形式

ID Pred
2021_3104_3112 0.5
2021_3104_3116 0.5

64チームのシングルエリミネーションに対して、全部の組み合わせについて予測をしていきます。ID列は[年チームID1チームID2]という構成で、若いチームIDが先にくる。若いチームIDのチームが勝つ確率を0~1でPredに設定します。

行数は64*63/2=2016で2016行です。

この中から実際に行われた試合64試合分の予測を使って評価が行われます。今回NCAAMの方はCOVID19の影響で1試合中止になり、63試合で集計となりました。

評価値

log loss

極端な間違いに対するペナルティが大きいことが特徴の評価値になります。

from sklearn.metrics import log_loss
log_loss([1],[0],labels=[0,1])

>>34.538776394910684

絶対勝つからpredに1を入れて結果が外れると34.5になるため、64試合で割るとscoreが0.54ほど上昇してしまいます。
そして、過去のコンペの優勝スコア1,2が0.4~0.5程度のため、大きな間違いは詰みです。

余談ですが、シードが1-16されていてこの数字は1から順に強いチームに割り当てられ、シード1の初戦はシード16のようにトーナメント表も決まっているみたいです。そしてシードが大きいチームが小さいチームに勝つことをupset(番狂わせ)と言い、発生率は男子が多く女子だと珍しいことから、女子のソリューションではシード1と16の組み合わせは1が100%勝つような予測を出すのが選択肢に入ってきます。

データと方針とpandasの勉強

都市や大会のマスタなども配られていますが、基本的にレギュラーシーズンの試合データとNCAAトーナメント(過去分)の試合データを使ってtrainingデータを作っていきます。

具体的にはトーナメントの結果をラベルとし、特徴量にその都市のレギュラーシーズンの成績を肉付けていきます

注意点としては、勝チーム情報と負けチーム情報というふうに列が分かれています。これだとどちらかのチームIDが勝率100%のデータになってしまうため、勝チーム情報と負けチーム情報を入れ替えたテーブルを結合して特徴として成り立つようにします。 ※正直これで大丈夫な根拠を理解できていないです。同じ試合のレコードが2個あるということになってないのかなぁ

チーム情報を入れ替えて一つのdataframeにするコード

# https://www.kaggle.com/raddar/paris-madness?scriptVersionId=9740989
def prepare_data(df):
    df_swap = df[['Season', 'DayNum', 'LTeamID', 'LScore', 'WTeamID', 'WScore', 'WLoc', 'NumOT', 
    'LFGM', 'LFGA', 'LFGM3', 'LFGA3', 'LFTM', 'LFTA', 'LOR', 'LDR', 'LAst', 'LTO', 'LStl', 'LBlk', 'LPF', 
    'WFGM', 'WFGA', 'WFGM3', 'WFGA3', 'WFTM', 'WFTA', 'WOR', 'WDR', 'WAst', 'WTO', 'WStl', 'WBlk', 'WPF']]
    
    # home awayの入れ替え
    # df.loc[行の条件, 列の条件]
    df_swap.loc[df['WLoc'] == 'H', 'WLoc'] = 'A'
    df_swap.loc[df['WLoc'] == 'A', 'WLoc'] = 'H'
    df.columns.values[6] = 'location'
    df_swap.columns.values[6] = 'location'
    
    # win loseでなくチーム1,チーム2にカラム名を変えておく
    # df.columnsにlistで設定ができる
    df.columns = [x.replace('W', 'T1_').replace('L', 'T2_') for x in list(df.columns)]
    df_swap.columns = [x.replace('L', 'T1_').replace('W', 'T2_') for x in list(df_swap.columns)]
    
    # 結合
    # pd.concat デフォルトでaxis=0(縦方向結合)
    output = pd.concat([df, df_swap]).reset_index(drop=True)
    output.loc[output.location=='N', 'location'] = '0'
    output.loc[output.location=='H', 'location'] = '1'
    output.loc[output.location=='A', 'location'] = '-1'
    output.location = output.location.astype(int)
    
    # PointDiff列を作成し、得点差を代入
    output['PointDiff'] = output['T1_Score'] - output['T2_Score']
    
    return output

レギュラーシーズンについて、各年のチーム毎に列ごとの平均値を計算する.
T2側の特徴量は複数のチームの成績がごちゃまぜになるが、これは対戦相手の成績という情報になる。こういう捉え方はソリューションを読むまで気づきませんでした。

# groupby(["Season", 'T1_TeamID']) 同じSeason T1_TeamIDでまとめる
# [boxscore_cols]使用する列をリストで指定
# agg(funcs) まとめ方を指定、今回は np.mean
funcs = [np.mean]
season_statistics = regular_data.groupby(["Season", 'T1_TeamID'])[boxscore_cols].agg(funcs).reset_index()

集計したデータについて、チーム1の特徴用とチーム2の特徴用に加工してくっつける作業をします。

# copy()をしないと参照渡しになってしまう
season_statistics_T1 = season_statistics.copy()
season_statistics_T2 = season_statistics.copy()

# 列名の加工
season_statistics_T1.columns = ["T1_" + x.replace("T1_","").replace("T2_","opponent_") for x in list(season_statistics_T1.columns)]
season_statistics_T2.columns = ["T2_" + x.replace("T1_","").replace("T2_","opponent_") for x in list(season_statistics_T2.columns)]
# 後処理
season_statistics_T1.columns.values[0] = "Season"
season_statistics_T2.columns.values[0] = "Season"

# 結合
tourney_data = pd.merge(tourney_data, season_statistics_T1, on = ['Season', 'T1_TeamID'], how = 'left')
tourney_data = pd.merge(tourney_data, season_statistics_T2, on = ['Season', 'T2_TeamID'], how = 'left')

同様にseasonとteamIDをキーに特徴量を作って、team1用とteam2用に加工して肉付けしていく流れになります。

結果

基本的にはraddarさんのノート3を写経して、少し特徴量を加えたり削ったりして、lgbmでも学習してアンサンブルを行いました。
女子の方はupsetが起きにくいということで、1回戦は第1~4シードが勝利だけでなく、2回戦も第1,2シードが勝利するというおみくじを仕込みましたが下記マッチアップでupsetが発生して爆死しました。もう一つのsubも特に振るうことはなかったです。

  • Arkansas[4]-Wright St.[13]

男子の方はmodelの出力そのままのものと、ゴンザガ大を全部勝率1にしたものをおみくじsubしました。決勝開始前時点ではおみくじsubが65位まで上がっていたのですが、決勝で負けてしまいmodelの出力そのままの方がスコアがいいということになりましたが、こちらもメダル圏外でした。

f:id:teyoblog:20210406204337p:plain
男子決勝前までの図
f:id:teyoblog:20210406204415p:plain
決勝前までのsubの様子
f:id:teyoblog:20210406204442p:plain
終結

感想

今回、スポーツ大会開催前にモデルで予測してそれを元に大会を楽しむという新しいものでした。日本からは無料で見る手段がなかったため、公式サイトのスコア速報を追ったりすることしか出来ませんでしたがゴンザガ大の準決勝はとてもハラハラしました。

スコア的には、基本的にメダルの圏外にいて、ゴンザガが勝てば少し上がるようなものだったので予測精度はほか参加者より劣っていたと思います。また力をつけていきたいです。

【本読んだ】機械学習のエッセンス

こんにちは

その道の人?がプッシュしていた1ので、購入して写経しながら読みました。
下記に自分の感想とは書きますが、この本の良さについてはプッシュされてる記事を読むほうが参考になります。(断言)

内容

  1. 学習の前に
  2. Pythonの基本
  3. 機械学習に必要な数学
  4. Pythonによる数値計算とデータの可視化
  5. 機械学習アルゴリズム

最終的にPython機械学習アルゴリズムを実装していくため、Pythonの基本的な部分を勉強する章があります。その後必要になってくる数学の勉強ができる章が続きます。
次に、Python数値計算を書いて実際に動かします。最後に機械学習アルゴリズムを数式で示しながら実装をしていくという流れです。 実際に実装するアルゴリズムは下記

  • 線形回帰
  • Ridge線形回帰
  • Lasso線形回帰
  • ロジスティック回帰
  • サポートベクタマシン
  • K-means法
  • 主成分分析(PCA)

読んでみて

  • 数学の抜けというか実力不足を感じた
    • 写像・・・?ってなった
  • 4章で実行速度などに焦点を当ててscipyの紹介をしているのが印象的だった
  • メインとも言える5章の数式とかが理解まで至らずちょこちょこ挫折しましたごめんなさい
    • 特にSVMの数式は頭が止まっちゃいました
  • また理論からしっかり理解する必要を感じたときに帰ってきます
  • 他の理論から学ぶ本を読んだことないので比較はできないですが、アルゴリズムを理解してpythonで使っていきたい人には充実した内容と思います

さいごに

数式に抵抗がなくなる本ないですか?