画像分類コンテストでスコアを上げるためのヒント
I.はじめに
この記事を読む前に、次の内容を読んでください:画像分類競争のベースライン - インテリジェント ハードウェア音声制御の時間周波数画像分類の課題を例として取り上げます。この記事では、イベントの背景とデータ セットの形式を紹介し、フライング パドルに基づいたベースラインを提供します。この記事のすべての操作はベースラインに基づいて実行されます。スコアを向上させるための次のヒントの主要な手順のみを掲載します。トレーニングにおける具体的な実装については、全員が検討する必要があります。
私の現在のスコアは0.94203で、長期的には 6 位にランクされています。
2. ポイントアップのヒント
2.1 データのセグメント化
ベースラインでは、元のデータをシャッフルし、最後の 200 個のデータを検証セットとして取得します。元のデータ内の各タイプのサンプル数は基本的に同じであるため、この方法ではトレーニング セット内の各タイプのサンプルに大きな差が生じ、モデルに「偏り」が生じます。検証セット内の各タイプのサンプルの差も大きくなり、検証の損失につながり、acc はモデルのパフォーマンスを正しく反映できなくなります。したがって、元のデータから検証セットと同じ数のサンプルを取得する必要があります。
#每一类抽取十张作为验证集
val_df = pd.DataFrame()
for i in range(24):
val_df = val_df.append(train_df[train_df['label']==i][-10:])
train_df = train_df.drop(labels=train_df[train_df['label']==i][-10:].index)
val_df = val_df.reset_index(drop=True)
train_df = train_df.reset_index(drop=True)
2.2 データの強化
元のデータが不足している場合、データの多様性を向上させるにはデータの拡張が非常に有効であり、今回のコンテストでは主にこの点に取り組みました。
paddle.vision.tranforms
ランダムなトリミング、ランダムな反転、ランダムな回転、正規化など、多くの一般的なデータ拡張方法が提供されます。使用方法も非常に簡単で、API を直接呼び出すだけです。ただし、API を提供していない非常に便利なデータ拡張メソッドも多数あります。ここでは、私が使用している 3 つの効果的なデータ拡張メソッド (ランダム消去、混合クラス拡張、クロッピングと混合) を紹介します。
2.2.1 ランダム消去
トレーニング中に、画像の長方形の領域がランダムに選択され、そのピクセルがランダムな値を使用して消去されます。オクルージョン レベルを使用してトレーニング画像を生成すると、オーバーフィッティングのリスクが軽減され、モデルがオクルージョンに対して堅牢になります。
# 随机擦除
def random_erase(img,n_holes,length,rate): #输入img为PIL图片格式的图片
if np.random.rand(1)[0]<rate:
img = np.array(img)
h = img.shape[0] #图片的高
w = img.shape[1] #图片的宽
n_holes = np.random.randint(n_holes)
mask = np.ones((h, w), np.float32) #32*32w*h的全1矩阵
for n in range(n_holes): #n_holes=2,length=4 选择2个区域;每个区域的边长为4
y = np.random.randint(h) #0~31随机选择一个数 y=4
x = np.random.randint(w) #0~31随机选择一个数 x=24
y1 = np.clip(y - length // 2, 0, h) #2,0,32 ->2
y2 = np.clip(y + length // 2, 0, h) #6,0,32 ->6
x1 = np.clip(x - length // 2, 0, w) #24-2,0,32 ->22
x2 = np.clip(x + length // 2, 0, w) #24+2,0,32 ->26
mask[y1: y2, x1: x2] = 0. #将这一小块区域去除
img[:,:,0] = img[:,:,0] * mask
img[:,:,1] = img[:,:,1] * mask
img[:,:,2] = img[:,:,2] * mask
return Image.fromarray(img)
else:
return img
入力パラメータ:
- img : PIL 形式の画像。
- n_holes : 正方形の最大数、つまり数値は 0 から n_holes までのランダムな値です。
- length : 正方形の辺の長さ。
- rate : ランダム消去の確率。
出力パラメータ:
- ランダム消去後の PIL 形式の画像
以下は効果を示す図です。
img = Image.open(train_df['path'].values[0]).convert('RGB')
img2 = random_erase(img,100,10,0.2)
2.2.2 混合拡張(Mixup)
2 つのサンプルの画像とラベルを比例して混合すると、サンプルの分布が拡大され、トレーニングされたモデルがより堅牢になります。
def random_mixup(img ,label, mixup_img, mixup_label):#输入img和mixup为IMG格式的图片,label和mixup_label为int类型
img = np.array(img)
mixup_img = np.array(mixup_img)
label_onehot = np.zeros(24)
label_onehot[label] = 1
mixup_label_onehot = np.zeros(24)
mixup_label_onehot[mixup_label] = 1
alpha = 1
lam = np.random.beta(alpha,alpha) #混合比例
img_new = lam*img + (1-lam)*mixup_img
label_new = lam*label_onehot + (1-lam)*mixup_label_onehot
return Image.fromarray(np.uint8(img_new)), paddle.to_tensor(np.float32(label_new))
入力パラメータ:
- img & mixup_img : 混合する PIL 形式の 2 つの画像。
- label & mixup_label : 2 つの画像に対応するラベル (int 形式)
出力パラメータ:
- PIL形式の混合画像
- 混合 Tensor タイプのラベル、one_hot としてエンコード
以下にその効果を示す 2 つの写真を示します。
img1 = Image.open(train_df['path'].values[0]).convert('RGB')
label1 = train_df['label'].values[0]
img2 = Image.open(train_df['path'].values[1]).convert('RGB')
label2 = train_df['label'].values[1]
img_new, label_new = random_mixup(img1, label1, img2, label2)
⭐mixupを使用するには、データラベルがone_hotエンコーディングである必要があり、損失関数がcriterion = nn.CrossEntropyLoss(soft_label=True)
one_hotエンコーディングの使用を示す に設定される必要があることに注意してください。
2.2.3 カットミックス
画像の一部を切り取って新たな入力画像として別の画像に重ね合わせて学習用ネットワークに入力し、画像内の2種類の要素の面積比に応じてラベルに重み付けを行って合計します。
def rand_bbox(size, lam):
if len(size) == 4:
W = size[2]
H = size[3]
elif len(size) == 3 or len(size) == 2:
W = size[0]
H = size[1]
else:
raise Exception
cut_rat = np.sqrt(1. - lam)
cut_w = np.int(W * cut_rat)
cut_h = np.int(H * cut_rat)
# uniform
cx = np.random.randint(W)
cy = np.random.randint(H)
bbx1 = np.clip(cx - cut_w // 2, 0, W)
bby1 = np.clip(cy - cut_h // 2, 0, H)
bbx2 = np.clip(cx + cut_w // 2, 0, W)
bby2 = np.clip(cy + cut_h // 2, 0, H)
return bbx1, bby1, bbx2, bby2
def cutmix(img ,label, cutmix_img, cutmix_label):
#int转化为one_hot
label_onehot = np.zeros(24)
label_onehot[label] = 1
cutmix_label_onehot = np.zeros(24)
cutmix_label_onehot[cutmix_label] = 1
alpha = 1
lam = np.random.beta(alpha,alpha)
bbx1, bby1, bbx2, bby2 = rand_bbox(img.size, lam)
img_new = img.copy()
img_new.paste(cutmix_img.crop((bbx1, bby1, bbx2, bby2)),(bbx1, bby1, bbx2, bby2))
# 计算 1 - bbox占整张图像面积的比例
lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (img_new.size[0] * img_new.size[1]))
label_new = lam*label_onehot + (1-lam)*cutmix_label_onehot
return img_new,paddle.to_tensor(np.float32(label_new))
入力パラメータ:
- img & Cutmix_img : 混合する PIL 形式の 2 つの画像。
- label & Cutmix_label : 2 つの画像に対応するラベル (int 形式)
出力パラメータ:
- PIL形式の混合画像
- 混合 Tensor タイプのラベル、one_hot としてエンコード
以下にその効果を示す 2 つの写真を示します。
img1 = Image.open(train_df['path'].values[0]).convert('RGB')
label1 = train_df['label'].values[0]
img2 = Image.open(train_df['path'].values[1]).convert('RGB')
label2 = train_df['label'].values[1]
img_new, label_new = cutmix(img1, label1, img2, label2)
2.2.4 正規化
正規化の機能は、処理する必要があるデータの値を、特定の処理方法によって特定の範囲に制限することです。深層学習画像処理では、正規化処理後、データは活性化関数によく応答し、データの表現力が向上し、勾配爆発と勾配消失の発生を減らすことができます。一般的な正規化処理は平均値0、分散1のガウス分布にデータを加工することであり、paddle.vision.transforms.Normalize()
この機能を実現できます。
画像が を通過するとpaddle.vision.transforms.ToTensor()
、画像は Tensor 形式に変換され、変換後の形状は (CxHxW) となり、値の範囲は [0,1] になります。平均と標準偏差はデータごとに異なるため、データセット全体にわたって 3 つのチャネルの平均と標準偏差を計算する必要があります。
#获取所有图片三通道的均值和方差
all_df = train_df.append(test_df,ignore_index=True).append(val_df, ignore_index=True)
all_dataset = XunFeiDataset(all_df['path'].values, all_df['label'].values,
transforms.Compose([
transforms.RandomCrop((450,750)),
transforms.ToTensor()
]),mode='test')
def getStat(train_data):
'''
Compute mean and variance for training data
:param train_data: 自定义类Dataset(或ImageFolder即可)
:return: (mean, std)
'''
print('Compute mean and variance for training data.')
print(len(train_data))
train_loader = DataLoader(
train_data, batch_size=1, shuffle=False, num_workers=0)
mean = np.zeros(3)
std = np.zeros(3)
for X, _ in train_loader:
for d in range(3):
mean[d] += X[:, d, :, :].mean().cpu().numpy()[0]
std[d] += X[:, d, :, :].std().cpu().numpy()[0]
mean = mean/len(train_data)
std = std/len(train_data)
return list(mean), list(std)
print(getStat(all_dataset))
実行後の出力は次のとおりです。
Compute mean and variance for training data.
3163
([0.6766141943829066, 0.06216619949979672, 0.2686088090203644], [0.24656723146663978, 0.14537001843179825, 0.17407946023116036])
計算されたデータにNormalize
パラメーターを入力するだけです。
Normalize((0.677, 0.062, 0.268), (0.246, 0.145, 0.174))
2.2.5 ラベルを滑らかにする
単純な正則化手法として、ラベル スムージングは、分類タスクにおけるモデルの一般化パフォーマンスと精度を向上させ、不均衡なデータ分布の問題を軽減できます。ラベル スムージング後、one_label は0 と 1 だけを持たなくなり、次の形式として理解できます。このクラスの確率。ラベルの平滑化により、モデルの信頼性が低下し、モデルが過剰適合によって発生する損失の深い亀裂に陥るのを防ぐことができます。
Paddle は Label Smooth API を提供します。
label_onehot = paddle.to_tensor(np.float32([0,0,1,0]))
nn.functional.label_smooth(label_onehot)
出力は次のとおりです。
Tensor(shape=[4], dtype=float32, place=Place(cpu), stop_gradient=True,
[0.02500000, 0.02500000, 0.92499995, 0.02500000])
2.2.6 変更されたデータセット
これらのデータ拡張メソッドを追加した後、データセットにもいくつかの変更を加える必要があります。
# 定义数据集读取方法
class XunFeiDataset(Dataset):
def __init__(self, img_path, label, transforms=None, mode='train'):
self.img_path = img_path
self.label = label
self.transforms = transforms
self.mode = mode
def __getitem__(self, index):
img = Image.open(self.img_path[index]).convert('RGB')
#将label转化为one_hot编码
label_onehot = np.zeros(24)
label_onehot[self.label[index]] = 1
label_onehot = paddle.to_tensor(np.float32(label_onehot))
if self.mode == 'train': #训练时才做数据增强
#随机擦除 100代表100个正方形,10代表每个正方形边长为10,0.2代表20%的概率
img = random_erase(img,100,10,0.2)
#mixup,0.2的概率
if np.random.rand(1)[0]<0.2:
mixup_idx = np.random.randint(0, len(self.img_path)-1)
mixup_img = Image.open(self.img_path[mixup_idx]).convert('RGB')
mixup_label = self.label[mixup_idx]
img, label_onehot = random_mixup(img, self.label[index], mixup_img, mixup_label)
#cutmix,0.2的概率
if np.random.rand(1)[0]<0.2:
cutmix_idx = np.random.randint(0, len(self.img_path)-1)
cutmix_img = Image.open(self.img_path[cutmix_idx]).convert('RGB')
cutmix_label = self.label[cutmix_idx]
img, label_onehot = cutmix(img, self.label[index], cutmix_img, cutmix_label)
if self.transforms is not None:
img = self.transforms(img)
label_onehot = nn.functional.label_smooth(label_onehot)
return img, label_onehot
def __len__(self):
return len(self.img_path)
2.3 学習率と最適化機能
学習率減衰法によりモデルの精度を効果的に向上させることができ、最適化関数には AdamW が使用されます。
Adamw は Adam + Weight Decate です。効果は Adam + L2 正則化と同じですが、 L2 正則化では損失に正規項を追加し、次に勾配を計算し、最後に逆伝播する必要があるのに対し、Adamw は勾配を直接正則化するため、計算効率が高くなります。の項がバックプロパゲーションの式に追加されるため、損失に通常の項を手動で追加する必要がなくなります。
scheduler = paddle.optimizer.lr.StepDecay(0.0001,15,gamma=0.1,verbose=False)
optimizer = paddle.optimizer.AdamW(learning_rate=scheduler, parameters=model.parameters())
学習率の減衰方法の詳細については、「Pytorch の学習率調整方法」を参照してください。また、その中の API フライング パドルも基本的に利用できます。
2.4 トレーニング方法
テスト後、5 分割相互検証メソッドにより予測精度が大幅に向上します。具体的な実装については、「ベースライン」を参照してください。テスト セットを予測するとき、検証セットで最も優れたパフォーマンスを発揮するパラメーターを使用して、検証セットの結果を予測できます。それぞれの折り目。
for i in range(k_fold):
······
for epoch in range(epoches):
······
if val_acc>max_acc:
test_pred = predict(test_loader, model, criterion)
max_acc = val_acc
3. 高得点への道
データの前処理/特徴エンジニアリング | モデル | 分数 |
---|---|---|
サイズ変更(256)、ランダムクロッピング(224)、正規化(ベースラインパラメータ) | レスネット18 | 0.80~0.86 |
ランダムクロップ (450,750)、正規化 (ベースラインパラメータ) | レスネット18 | 0.91126 |
ランダムトリミング (450,750)、ランダム消去、正規化 (ベースラインパラメータ) | レスネット18 | 0.91533 |
検証セットの再分割、ランダムなトリミング (450,750)、ランダムな消去、ミックスアップ、正規化 (パラメータの更新) | レスネット34 | 0.92375 |
検証セットの再分割、ランダム クロッピング (450,750)、ランダム消去、ミックスアップ、正規化 (パラメータの更新)、5 分割相互検証 | レスネット34 | 0.93375 |
検証セットの再分割、ランダム クロッピング (450,750)、ランダム消去、ミックスアップ、label_smooth、正規化 (パラメーターの更新)、5 分割相互検証 | レスネット34 | 0.93518 |
検証セットの再分割、ランダム クロップ (450,750)、ランダム消去、ミックスアップ、カットミックス、label_smooth、正規化 (パラメーターの更新) | レスネット34 | 0.93509 |
検証セットの再分割、ランダム クロップ (450,750)、ランダム消去、ミックスアップ、カットミックス、label_smooth、正規化 (パラメーターの更新)、5 分割相互検証 | レスネット34 | 0.94203 |
4. 最後に
これで、このコンテストでの高得点への道は終了となります。今回は主にデータ特徴量エンジニアリングなどの側面から開始し、モデルにはあまり変更を加えませんでした。これをベースに、モデル構造の改善を検討できます。より高いスコアを取得するには、さらに最適化します。