{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"这是一个时尚图片的分类任务，数据是各种商品图片。标签是商品种类。\n\n这个notebook是基本图像分类任务的baseline,包含了数据加载，增强，模型训练，验证，测试等部分。\n\n代码基于[MinimalAI](https://github.com/EvilPsyCHo/MinimalAI/blob/main/2022%E5%B9%B4%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89SOTA%E5%AE%9E%E8%B7%B5.ipynb)修改得来，[对应的视频讲解地址](https://www.bilibili.com/video/BV1bq4y1a75r?spm_id_from=333.337.search-card.all.click&vd_source=6e04a3f7abafdcdb0370500cdb1e2a54)","metadata":{}},{"cell_type":"markdown","source":"# 导入相应库","metadata":{}},{"cell_type":"code","source":"import os\nimport sys\nsys.path.append('../input/timm-pytorch-image-models/pytorch-image-models-master')\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom matplotlib import pyplot as plt\nimport seaborn as sns\nimport time\nimport random\nimport shutil\n\nimport scipy as sp\nfrom sklearn.model_selection import StratifiedKFold, GroupKFold, KFold\nfrom tqdm.auto import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.optim import Adam, SGD,AdamW\nimport torchvision.models as models\nfrom torch.utils.data import DataLoader, Dataset\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts, CosineAnnealingLR, ReduceLROnPlateau\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\n#import loss_func\nfrom torch.cuda.amp import autocast, GradScaler\nimport warnings\nwarnings.filterwarnings('ignore')\n","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:38.034529Z","iopub.execute_input":"2022-08-01T08:17:38.035266Z","iopub.status.idle":"2022-08-01T08:17:42.313670Z","shell.execute_reply.started":"2022-08-01T08:17:38.035172Z","shell.execute_reply":"2022-08-01T08:17:42.312521Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义基本配置","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# CFG\n# ====================================================\nclass CFG:\n    apex = True\n    debug = False\n    print_freq = 200\n    num_workers = 2\n    model_name = 'tf_efficientnet_b2'\n    size_w = 224\n    size_h = 224#819\n    scheduler = 'CosineAnnealingLR'  # ['ReduceLROnPlateau', 'CosineAnnealingLR', 'CosineAnnealingWarmRestarts']\n    epochs = 5\n    # factor=0.2 # ReduceLROnPlateau\n    # patience=4 # ReduceLROnPlateau\n    # eps=1e-6 # ReduceLROnPlateau\n    T_max = 5  # CosineAnnealingLR8\n    T_0=5   # CosineAnnealingWarmRestarts\n    lr = 3e-4\n    min_lr = 1e-6\n    batch_size = 64\n    weight_decay = 1e-6\n    gradient_accumulation_steps = 1\n    max_grad_norm = 1000\n    seed = 42\n    target_col = 'target'\n    n_fold = 5\n    trn_fold = [0,1,2,3,4]\n    train = True\n    \n\ntrain = pd.read_csv('../input/boolart-image-classification/train.csv')\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nOUTPUT_DIR = './'\nif not os.path.exists(OUTPUT_DIR):\n    os.makedirs(OUTPUT_DIR)\n\nif CFG.debug:\n    CFG.epochs = 3\n    train = train.sample(n=1000, random_state=CFG.seed).reset_index(drop=True)\n\ndef seed_torch(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_torch(seed=CFG.seed)\n\nFold = StratifiedKFold(n_splits=CFG.n_fold, shuffle=True, random_state=CFG.seed)\n#labellll = np.array(train[CFG.target_col]).astype(np.int)\nfor n, (train_index, val_index) in enumerate(Fold.split(train, np.array(train[CFG.target_col]))):\n    train.loc[val_index, 'fold'] = int(n)\ntrain['fold'] = train['fold'].astype(int)","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:42.316025Z","iopub.execute_input":"2022-08-01T08:17:42.317087Z","iopub.status.idle":"2022-08-01T08:17:42.439916Z","shell.execute_reply.started":"2022-08-01T08:17:42.317046Z","shell.execute_reply":"2022-08-01T08:17:42.438963Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 加载训练和验证数据\n","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# Dataset \n# ====================================================\nclass TrainDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.file_names = df['id'].values\n        self.labels = df[CFG.target_col].values\n        self.transform = transform\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        self.file_path = f'../input/boolart-image-classification/train_image/{self.file_names[idx]}.jpg'\n        image = np.array(Image.open(self.file_path).convert(\"RGB\"))\n        if self.transform:\n            image = self.transform(image=image)['image']\n        else:\n            image = cv2.resize(image, (CFG.size, CFG.size))\n            image = image[np.newaxis, :, :]\n            image = torch.from_numpy(image).float()\n        label = torch.tensor(self.labels[idx]).long()\n        return image/255, label\n\n# ====================================================\n# Transforms 定义数据增强\n# ====================================================\ndef get_transforms(*, data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(CFG.size_w, CFG.size_h),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.OneOf([\n                A.IAAAdditiveGaussianNoise(),\n                A.GaussNoise(), ], p=0.2),\n            A.OneOf([\n                A.MotionBlur(p=.2),\n                A.MedianBlur(blur_limit=3, p=.1),\n                A.Blur(blur_limit=2, p=.1), ], p=0.2),\n            A.OneOf([A.OpticalDistortion(p=0.3),\n                     A.GridDistortion(p=.1),\n                     A.IAAPiecewiseAffine(p=0.3), ], p=0.2),\n            A.CoarseDropout(max_holes=8, max_height=int(224 * 0.1), max_width=int(224 * 0.1), p=0.5),\n            ToTensorV2(),\n        ])\n\n    elif data == 'valid':\n        return A.Compose([\n            A.Resize(CFG.size_w, CFG.size_h),\n            ToTensorV2(),\n        ])","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:42.442599Z","iopub.execute_input":"2022-08-01T08:17:42.442987Z","iopub.status.idle":"2022-08-01T08:17:42.460368Z","shell.execute_reply.started":"2022-08-01T08:17:42.442948Z","shell.execute_reply":"2022-08-01T08:17:42.459380Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义模型","metadata":{"execution":{"iopub.status.busy":"2022-07-28T09:53:53.095644Z","iopub.execute_input":"2022-07-28T09:53:53.095978Z","iopub.status.idle":"2022-07-28T09:53:53.100647Z","shell.execute_reply.started":"2022-07-28T09:53:53.095951Z","shell.execute_reply":"2022-07-28T09:53:53.099647Z"}}},{"cell_type":"code","source":"class CustomModel(nn.Module):\n    def __init__(self, cfg, pretrained=False):\n        super().__init__()\n        self.cfg = cfg\n        self.model = timm.create_model(self.cfg.model_name, pretrained=pretrained, in_chans=3)\n        #print(self.model)\n        if 'efficientnet' in self.cfg.model_name:\n            self.n_features = self.model.classifier.in_features\n            self.model.global_pool = nn.Identity()\n            self.model.classifier = nn.Identity()\n        elif 'resnet' in self.cfg.model_name:\n            self.n_features = self.model.fc.in_features\n            self.model.global_pool = nn.Identity()\n            self.model.fc = nn.Identity()\n        elif 'convnext' in self.cfg.model_name:\n            self.n_features = self.model.head.fc.in_features\n            self.model.head = nn.Identity()\n            self.model.fc = nn.Identity()\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.classifier = nn.Sequential(\n                            #nn.Conv2d(self.n_features, self.n_features // 8, 1),\n                            #nn.LeakyReLU(),\n                            #nn.BatchNorm2d(self.n_features // 8),\n                            nn.Conv2d(self.n_features, 44, 1),\n                            #nn.Sigmoid()\n                        )\n\n    def forward(self, x):\n        bs = x.size(0)\n        features = self.model(x)\n        pool_feature = self.pooling(features)\n        output = self.classifier(pool_feature).view(bs, -1)\n        return output","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:42.463403Z","iopub.execute_input":"2022-08-01T08:17:42.463898Z","iopub.status.idle":"2022-08-01T08:17:42.477037Z","shell.execute_reply.started":"2022-08-01T08:17:42.463847Z","shell.execute_reply":"2022-08-01T08:17:42.476087Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# ====================================================\n# loader\n# ====================================================\nfold = 0\nfolds = train\ntrn_idx = folds[folds['fold'] != fold].index\nval_idx = folds[folds['fold'] == fold].index\ntrain_folds = folds.loc[trn_idx].reset_index(drop=True)\nvalid_folds = folds.loc[val_idx].reset_index(drop=True)\nvalid_labels = valid_folds[CFG.target_col].values\n\ntrain_dataset = TrainDataset(train_folds,\n                             transform=get_transforms(data='train'))\nvalid_dataset = TrainDataset(valid_folds,\n                             transform=get_transforms(data='valid'))\n\ntrain_loader = DataLoader(train_dataset,\n                          batch_size=CFG.batch_size,\n                          shuffle=True,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=False)\nvalid_loader = DataLoader(valid_dataset,\n                          batch_size=CFG.batch_size * 2,\n                          shuffle=False,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=False)","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:42.478668Z","iopub.execute_input":"2022-08-01T08:17:42.479048Z","iopub.status.idle":"2022-08-01T08:17:42.510976Z","shell.execute_reply.started":"2022-08-01T08:17:42.479012Z","shell.execute_reply":"2022-08-01T08:17:42.509231Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 查看增强后的图片效果","metadata":{}},{"cell_type":"code","source":"def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):\n    \"\"\"绘制图像列表\"\"\"\n    figsize = (num_cols * scale, num_rows * scale)\n    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)\n    axes = axes.flatten()\n    for i, (ax, img) in enumerate(zip(axes, imgs)):\n        if torch.is_tensor(img):\n            # 图片张量\n            img = img.permute(1,2,0).numpy()*255\n            ax.imshow(img.astype(np.uint8))\n        else:\n            # PIL图片\n            ax.imshow(img)\n        ax.axes.get_xaxis().set_visible(False)\n        ax.axes.get_yaxis().set_visible(False)\n        ax.set_title(y[i].item())\n    return axes\n\nX, y = next(iter(train_loader))\nshow_images(X, 8, 8, y);","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:42.513181Z","iopub.execute_input":"2022-08-01T08:17:42.513856Z","iopub.status.idle":"2022-08-01T08:17:53.627193Z","shell.execute_reply.started":"2022-08-01T08:17:42.513820Z","shell.execute_reply":"2022-08-01T08:17:53.626197Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义训练和验证流程","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# train,valid\n# ====================================================\ndef train_fn(train_loader, model, criterion, optimizer, epoch, scheduler, device):\n    if CFG.apex:\n        scaler = GradScaler()\n    # switch to train mode\n    model.train()\n    global_step = 0\n    for step, (images, labels) in enumerate(train_loader):\n        images = images.to(device)\n        labels = labels.to(device)\n        batch_size = labels.size(0)\n        y_preds = model(images)\n        loss = criterion(y_preds, labels)\n\n        if CFG.gradient_accumulation_steps > 1:\n            loss = loss / CFG.gradient_accumulation_steps\n        if CFG.apex:\n            scaler.scale(loss).backward()\n        else:\n            loss.backward()\n        grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), CFG.max_grad_norm)\n        if (step + 1) % CFG.gradient_accumulation_steps == 0:\n            if CFG.apex:\n                scaler.step(optimizer)\n                scaler.update()\n            else:\n                optimizer.step()\n            optimizer.zero_grad()\n            global_step += 1\n        if step % CFG.print_freq == 0 or step == (len(train_loader)-1):\n            print('Epoch: [{0}][{1}/{2}] '\n                  'Loss: {loss:.4f}'\n                  .format(\n                   epoch+1, step, len(train_loader),loss=loss.item(),\n                   ))\n\n\ndef valid_fn(valid_loader, model, criterion, device):\n    # switch to evaluation mode\n    model.eval()\n    preds = []\n    acc = 0.\n    for step, (images, labels) in enumerate(valid_loader):\n        images = images.to(device)\n        labels = labels.to(device)\n        batch_size = labels.size(0)\n        # TTA\n        with torch.no_grad():\n            outputs1 = model(images)\n            outputs2 = model(images.flip(-1))\n            outputs3 = model(images.flip(-2))\n            outputs4 = model(images.flip([-2, -1]))\n            outputs5 = model(images.flip(-1).flip([-2, -1]))\n            outputs6 = model(images.flip(-2).flip([-2, -1]))\n            outputs = (outputs1 + outputs2 + outputs3 + outputs4 + outputs5 + outputs6) / 6\n            loss = criterion(outputs, labels.long())\n            _, predict_y = torch.max(outputs, dim=1)\n            acc += (predict_y.to(device) == labels.to(device)).sum().item()\n        if CFG.gradient_accumulation_steps > 1:\n            loss = loss / CFG.gradient_accumulation_steps\n        # measure elapsed time\n        if step % CFG.print_freq == 0 or step == (len(valid_loader)-1):\n            print('EVAL: [{0}/{1}] '\n                  'Loss: {loss:.4f}'\n                  .format(step, len(valid_loader), loss=loss.item(),\n                   ))\n    return loss, acc","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:53.628340Z","iopub.execute_input":"2022-08-01T08:17:53.628782Z","iopub.status.idle":"2022-08-01T08:17:53.652348Z","shell.execute_reply.started":"2022-08-01T08:17:53.628742Z","shell.execute_reply":"2022-08-01T08:17:53.651343Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 加载数据、模型、优化器、学习率策略、损失函数进行训练","metadata":{}},{"cell_type":"code","source":"# scheduler\n# ====================================================\ndef get_scheduler(optimizer):\n    if CFG.scheduler == 'ReduceLROnPlateau':\n        scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=CFG.factor, patience=CFG.patience, verbose=True,\n                                      eps=CFG.eps)\n    elif CFG.scheduler == 'CosineAnnealingLR':\n        scheduler = CosineAnnealingLR(optimizer, T_max=CFG.T_max, eta_min=CFG.min_lr, last_epoch=-1)\n    elif CFG.scheduler == 'CosineAnnealingWarmRestarts':\n        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=CFG.T_0, T_mult=1, eta_min=CFG.min_lr, last_epoch=-1)\n    return scheduler\n\n# ====================================================\n# model & optimizer\n# ====================================================\nmodel = CustomModel(CFG, pretrained=True)\nmodel.to(device)\n\n#optimizer = SGD(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay,momentum=0.9)\noptimizer = AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay, amsgrad=False)\nscheduler = get_scheduler(optimizer)\n\ncriterion = nn.CrossEntropyLoss()\nbest_score = 0.\nbest_loss = np.inf\nfor epoch in range(CFG.epochs):\n    # train\n    train_fn(train_loader, model, criterion, optimizer, epoch, scheduler, device)\n    # eval\n    avg_val_loss, acc = valid_fn(valid_loader, model, criterion, device)\n    acc = acc / len(valid_dataset)\n    if isinstance(scheduler, ReduceLROnPlateau):\n        scheduler.step(avg_val_loss)\n    elif isinstance(scheduler, CosineAnnealingLR):\n        scheduler.step()\n    elif isinstance(scheduler, CosineAnnealingWarmRestarts):\n        scheduler.step()\n        \n    print(f\"epoch:{epoch+1}，acc:{acc}\")\n    if acc > best_score:\n        best_score = acc\n        torch.save({'model': model.state_dict(),\n                    'preds': acc},\n                   OUTPUT_DIR + f'{CFG.model_name}_fold{fold}_best_score.pth')\n\n    if avg_val_loss < best_loss:\n        best_loss = avg_val_loss\n        torch.save({'model': model.state_dict(),\n                    'preds': acc},\n                   OUTPUT_DIR + f'{CFG.model_name}_fold{fold}_best_loss.pth')","metadata":{"execution":{"iopub.status.busy":"2022-08-01T08:17:53.654595Z","iopub.execute_input":"2022-08-01T08:17:53.655383Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def show_preimage(imgs,y,pre, num_rows, num_cols, scale=1.5):\n    figsize = (num_cols * scale, num_rows * scale)\n    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)\n    axes = axes.flatten()\n    for i, (ax, img) in enumerate(zip(axes, images.cpu())):\n        if torch.is_tensor(img):\n            # 图片张量\n            img = img.permute(1,2,0).numpy()*255\n            ax.imshow(img.astype(np.uint8))\n        else:\n            # PIL图片\n            ax.imshow(img)\n        ax.axes.get_xaxis().set_visible(False)\n        ax.axes.get_yaxis().set_visible(False)\n        ax.text(30, 0, s=f\"y:{y[i].item()}\")\n        ax.text(100, 0, s=f\"pre:{pre[i].item()}\")","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 查看一个批次样本效果","metadata":{"execution":{"iopub.status.busy":"2022-07-28T09:56:53.621833Z","iopub.execute_input":"2022-07-28T09:56:53.622493Z","iopub.status.idle":"2022-07-28T09:56:53.628328Z","shell.execute_reply.started":"2022-07-28T09:56:53.622458Z","shell.execute_reply":"2022-07-28T09:56:53.626903Z"}}},{"cell_type":"code","source":"images, labels = next(iter(valid_loader))\nimages = images.to(device)\nlabels = labels.to(device)\nwith torch.no_grad():\n    outputs1 = model(images)\n_, predict_y = torch.max(outputs1, dim=1)\nshow_preimage(images,labels,predict_y,8,8)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 加载测试数据","metadata":{}},{"cell_type":"code","source":"TEST = '../input/boolart-image-classification/test_image/'\ntest_df = pd.read_csv('../input/boolart-image-classification/sample_submission.csv')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df['id'].values\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        self.file_path = TEST + f\"{self.df[idx]}.jpg\"\n        image = np.array(Image.open(self.file_path).convert(\"RGB\"))\n        #image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        if self.transform:\n            image = self.transform(image=image)['image']\n        else:\n            image = image[np.newaxis, :, :]\n            image = torch.from_numpy(image).float()\n        return image/255,self.df[idx]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义推理流程","metadata":{}},{"cell_type":"code","source":"def inference(model, models_path, test_loader, device):\n\n    tk0 = tqdm(enumerate(test_loader), total=len(test_loader))\n    pre = []\n    image_id = []\n    for i, (images,img_ids) in tk0:\n        image_id += list(img_ids.numpy())\n        images = images.to(device)\n#         avg_preds = []\n        for model_path in models_path:\n            model.load_state_dict(torch.load(model_path)['model'])\n            model.eval()\n            with torch.no_grad():\n                y_preds1 = F.softmax(model(images))\n                y_preds2 = F.softmax(model(images.flip(-1)))\n                y_preds3 = F.softmax(model(images.flip(-2)))\n                y_preds4 = F.softmax(model(images.flip([-2, -1])))\n                y_preds5 = F.softmax(model(images.flip(-1).flip([-2, -1])))\n                y_preds6 = F.softmax(model(images.flip(-2).flip([-2, -1])))\n            y_preds = (y_preds1.to('cpu').numpy() + y_preds2.to('cpu').numpy() +\n                       y_preds3.to('cpu').numpy() + y_preds4.to('cpu').numpy() + y_preds5.to(\n                        'cpu').numpy() + y_preds6.to('cpu').numpy()) / 6\n        avg_preds = F.softmax(torch.from_numpy(y_preds),dim=1)\n        _,predict_y = torch.max(avg_preds,dim = 1)\n        predict_y = np.array(predict_y).tolist()\n        pre += predict_y\n    return pre,image_id","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_dataset = TestDataset(test_df, transform=get_transforms(data='valid'))\ntest_loader = DataLoader(test_dataset, batch_size=CFG.batch_size, shuffle=False,\n                         num_workers=CFG.num_workers, pin_memory=True)\nmodels_path = ['./tf_efficientnet_b2_fold0_best_score.pth']\npredictions,img_id = inference(model, models_path, test_loader, device)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# submission\ndf = pd.DataFrame({\n    \"id\": img_id,\n    \"predict\": predictions\n})\ndf.to_csv(\"./submission.csv\", index=False)\ndf","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 优化策略","metadata":{}},{"cell_type":"markdown","source":"1. 调节CFG的基本配置，学习率大小（lr），bt大小（batch_size），学习率策略（scheduler）\n2. 调整backbone(当前使用model_name = 'tf_efficientnet_b2',可以调整为eff其他大小网络b1-b7,resnet50,或者convnext_small等较新的网络)\n3. 调整TTA(减少或增加TTA),[TTA介绍](https://medium.com/analytics-vidhya/test-time-augmentation-using-pytorch-3da02d0a3188)\n4. 模型融合","metadata":{}}]}