YOLO-V5 分類戦闘シリーズ - 独自のデータセットのチューニング
1. トレーニング画像とテスト画像を保存する
質問 1: トレーニング プロセス中に、トレーニング セットのデータ強化と検証セットのデータ強化が基本的に同じであることを確認するにはどうすればよいですか?
質問 2: トレーニング プロセス中に、トレーニング セットと検証セットのラベルが正しいことを確認するにはどうすればよいですか?
解決策: トレーニングとテストのプロセス中に、トレーニング画像とテスト画像を保存し、視覚化するために画像にラベルを描画します。
トレーニング プロセス中に、トレーニング データの前処理がテスト データの前処理と一貫していることを確認する必要があります。このステップは依然として非常に重要であり、最初にその一貫性を検証する必要があります。一般的な [前処理] には、画像サイズの調整 (サイズ変更、トリミング)、色の強調、空間変換 (反転、回転など)、正規化などが含まれます。
- 次のコード スニペットは、データ読み取りとデータ拡張を含む、データ読み取りのコア クラスです。コードの場所: [ ]に追加する
utils/dataloaders.py
必要があり、画像を保存する呼び出し場所に追加され、以下のコードがすべて追加されます (赤枠内のコード)def __init__()
self.augment = augment
def __getitem__()
self.save_images(f, sample)
-
画像のトレーニングとテストのためのコード スニペットは次のとおりです。
ClassificationDataset
クラスのメンバー関数に追加するだけです。トレーニング画像かテスト画像かを判断してself.augment
画像に名前を付けます。保存したパスは自分で設定できますが、def save_images(self, f, sample): # 保存数据增强后的图片,用于对比训练和验证的图片预处理是否一致 print(' -------------self.augment: ', self.augment) if self.augment: print(' -------------f: ', f) a = f.split('/train/')[0]+'/train_aug/'+os.path.basename(f)[:-4]+'_train.jpg' print(' -------------a: ', a) torchvision.utils.save_image(sample, a) else: print(' -------------f: ', f) a = f.split('/test/')[0]+'/train_aug/'+os.path.basename(f)[:-4]+'_test.jpg' print(' -------------a: ', a) torchvision.utils.save_image(sample, a)
トレーニング プロセス中に、テストおよびトレーニングの前処理で保存されるピクチャは次のとおりです。トレーニングセットとテスト セットのピクチャは同じピクチャを使用していることに注意してください。これは比較するのに便利です。
2. データの正規化
コードのデフォルトは、次のように[ ImageNet ] データセットの平均と標準偏差です。
IMAGENET_MEAN = 0.485, 0.456, 0.406 # RGB mean
IMAGENET_STD = 0.229, 0.224, 0.225 # RGB standard deviation
独自のデータセットを使用する場合、通常は、データセットが ImageNet と異なるかどうかに応じて、平均と標準偏差を再計算するかどうかを決定します。私自身のデータセットの場合、再計算する必要があります。コードは次のとおりです。
def compute_mean_std():
# 将所有的【路径+图片】读取到列表 all_file
images_path = 'datasets/biaozhu_train/train/' # 针对自己的路径,需要修改
all_file = []
for i,j,k in os.walk(images_path):
if len(k)>0:
print('i: ', i)
print('j: ', j)
all_file.append([os.path.join(i,kk) for kk in k])
print('------------------')
# 统计所有图片的数量
num_images = len(all_file[0]) + len(all_file[1]) + len(all_file[2])
# 逐张图计算均值和方差
mean_r = mean_g = mean_b = 0
std_r = std_g = std_b = 0
for i in range(3):
for j in range(len(all_file[i])):
print(' i: ', i)
print(' j: ', j)
img = cv2.imread(all_file[i][j])
print('img shape: ', img.shape)
img = np.asarray(img)
img = img.astype(np.float32) / 255.
# cv2
im_mean, im_stds = cv2.meanStdDev(img)
mean_r += im_mean[0]
mean_g += im_mean[1]
mean_b += im_mean[2]
std_r += im_stds[0]
std_g += im_stds[1]
std_b += im_stds[2]
# 计算平均值
aa_r = mean_r/num_images
aa_g = mean_g/num_images
aa_b = mean_b/num_images
bb_r = std_r/num_images
bb_g = std_g/num_images
bb_b = std_b/num_images
print(' last mean: ', aa_r,aa_g,aa_b)
print(' last stds: ', bb_r,bb_g,bb_b)
計算結果は以下の通りで、 [ ImageNet ]データセットの平均値とは異なります
IMAGENET_MEAN = 0.3539317, 0.36820036, 0.37243055
IMAGENET_STD = 0.22637626, 0.22498632, 0.22197094
注目に値します: 自分で計算した平均と標準偏差を使用すると、トレーニング プロセスがより安定し、変動が少なくなります。
3. データの強化
albumentations
公式のソース コードのトレーニング プロセスでは、トレーニング プロセスと検証プロセス (ライブラリが開発環境にインストールされている場合) の 2 つのデータ処理ライブラリがあり、それらは [ albumentations
] と [ torchvision
] です。それ以外の場合は、デフォルトで [ ] が使用されますtorchvision
。以下にそれぞれ紹介します。
3.1. データ拡張ライブラリ: アルバム
トレーニング中 (albumentations
ライブラリがインストールされている場合)、以下の画像のコードが実行されます。図に示すように、3 種類のデータ補正が含まれており、これも一般的に使用される画像データ補正方法です。
以下に、上記のデータ拡張方法をそれぞれ紹介します。
-
ランダムなトリミング
A.RandomResizedCrop(height=size, width=size, scale=scale, ratio=ratio)
関数 function : 最初に画像をトリミングし、次に固定サイズにリサイズします
height : 処理後の最終画像の高さ;
width : 処理後の最終画像の幅;
scale : トリミングされた画像の領域画像領域全体を占める (h* w の比率は何ですか)、
ratio : トリミング領域のアスペクト比。この関数のコアソースコードの一部を以下に示します.主に[切り出す領域のサイズ]と[切り抜きの開始位置]を計算します.詳細といくつかの注意事項は次のとおりです.
def get_params_dependent_on_targets(self, params): img = params["image"] area = img.shape[0] * img.shape[1] # 当【if】条件不满足时,尝试10次 for _attempt in range(10): # 根据scale和ratio参数,得到随机的参数,进而计算裁剪图的大小(w,h) target_area = random.uniform(*self.scale) * area log_ratio = (math.log(self.ratio[0]), math.log(self.ratio[1])) aspect_ratio = math.exp(random.uniform(*log_ratio)) w = int(round(math.sqrt(target_area * aspect_ratio))) # skipcq: PTC-W0028 h = int(round(math.sqrt(target_area / aspect_ratio))) # skipcq: PTC-W0028 # 计算裁剪区域在原图的起始点 if 0 < w <= img.shape[1] and 0 < h <= img.shape[0]: i = random.randint(0, img.shape[0] - h) j = random.randint(0, img.shape[1] - w) # 进行裁剪+resize操作,底层使用的OpenCV return { "crop_height": h, "crop_width": w, "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), } # 尝试10次后,如果上述的【if】条件依然无法满足,则执行下面的代码进行裁剪 # Fallback to central crop in_ratio = img.shape[1] / img.shape[0] if in_ratio < min(self.ratio): w = img.shape[1] h = int(round(w / min(self.ratio))) elif in_ratio > max(self.ratio): h = img.shape[0] w = int(round(h * max(self.ratio))) else: # whole image w = img.shape[1] h = img.shape[0] i = (img.shape[0] - h) // 2 j = (img.shape[1] - w) // 2 # 进行裁剪+resize操作,底层使用的OpenCV return { "crop_height": h, "crop_width": w, "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), }
元の画像をテストする
次の 3 枚の画像は、それぞれスケール パラメータ 0.06、0.5、0.9 を使用して取得されたトリミング画像であり、最終的に 256 にリサイズされています。ランダムに切り取っているため、切り取られた面積に大きな差があります -
空間変換
A.HorizontalFlip(p=hflip) # 左右翻转 A.VerticalFlip(p=vflip) # 上下翻转
処理結果は下図のようになります(左図:左右反転、右図:上下反転)。
-
色変更
A.ColorJitter(*color_jitter, 0) # 随机改变图像的亮度,对比度,饱和度
紹介なし
-
正規化された
A.Normalize(mean=mean, std=std) # Normalize and convert to Tensor
分類モデルの場合、次の図に示すように、通常は平均を減算し、標準偏差で割る必要があります。
-
入力アラインメントを調整する
ToTensorV2()
関数: (1) HWC から CHW へ; (2) numpy から tensor へ
関数のコアソースコードは次のとおりです。
def apply(self, img, **params): # skipcq: PYL-W0613 if len(img.shape) not in [2, 3]: raise ValueError("Albumentations only supports images in HW or HWC format") if len(img.shape) == 2: img = np.expand_dims(img, 2) return torch.from_numpy(img.transpose(2, 0, 1))
3.2. データ拡張ライブラリ: torchvision
# 官方的预处理方法
return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
-
中央のトリミング: CenterCrop(size) は、
長辺の中央領域を最小の辺のサイズでトリミングします。コアのコードは次のとおりですclass CenterCrop: # YOLOv5 CenterCrop class for image preprocessing, i.e. T.Compose([CenterCrop(size), ToTensor()]) def __init__(self, size=640): super().__init__() self.h, self.w = (size, size) if isinstance(size, int) else size def __call__(self, im): # im = np.array HWC imh, imw = im.shape[:2] m = min(imh, imw) # min dimension top, left = (imh - m) // 2, (imw - m) // 2 return cv2.resize(im[top:top + m, left:left + m], (self.w, self.h), interpolation=cv2.INTER_LINEAR)
-
入力配置の調整
Function 関数: (1) HWC から CHW; (2) BGR から RGB; (3) numpy から tensor;
関数のコア ソース コードは次のとおりです。class ToTensor: # YOLOv5 ToTensor class for image preprocessing, i.e. T.Compose([LetterBox(size), ToTensor()]) def __init__(self, half=False): super().__init__() self.half = half def __call__(self, im): # im = np.array HWC in BGR order im = np.ascontiguousarray(im.transpose((2, 0, 1))[::-1]) # HWC to CHW -> BGR to RGB -> contiguous im = torch.from_numpy(im) # to torch im = im.half() if self.half else im.float() # uint8 to fp16/32
T.Normalize(IMAGENET_MEAN, IMAGENET_STD)]
:紹介なし
4. ONNX CPU の推論
主に次の 3 つの部分で構成されます。
- Pt から ONNX モデルへ。
- ONNX 推論検証 (Python);
- ONNX CPU 推論 (C++)。
4.1、Pt モデルを ONNX に変換
コードの場所:yolov5/export.py
# 针对自己的网络输入大小,注意 imgsz 的设置,默认是640
python export.py --weights runs/train-cls/exp24/weights/best.pt --include onnx --imgsz 256
4.2. ONNX 推論の検証
コードの場所:yolov5/classify/val.py
python classify/val.py --weights best.onnx
4.3、ONNX CPU 推論 (C++)
main.cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <onnxruntime_cxx_api.h>
#include "helpers.h"
#include<algorithm>
using namespace cv;
using namespace std;
int main(int argc, char* argv[]) {
if (argc < 4)
{
std::cout << ">>>>>>>>>>>>>>>>>>>>>>>>>: Parameters too less" << std::endl;
return -1;
}
std::string class_path = argv[2];
std::string image_path = argv[3];
Ort::Env env;
Ort::Session session(nullptr);
Ort::SessionOptions sessionOptions{
nullptr};
constexpr int64_t numChannles = 3;
constexpr int64_t width = 256;
constexpr int64_t height = 256;
constexpr int64_t numClasses = 2;
constexpr int64_t numInputElements = numChannles * height * width;
const string imgFile = image_path;
const string labelFile = class_path;
auto modelPath = argv[1];
sessionOptions = Ort::SessionOptions();
//load labels
Helpers utils;
vector<string> labels = utils.loadLabels(labelFile);
if (labels.empty()){
cout<< "Failed to load labels: " << labelFile << endl;
return 1;
}
//load image
vector<float> imageVec = utils.loadImage(imgFile, height, width);
if (imageVec.empty()){
cout << "Invalid image format. Must be 224*224 RGB image. " << endl;
return 1;
}
// create session
cout << "create session. " << endl;
session = Ort::Session(env, modelPath, sessionOptions);
// get the number of input
size_t num_input_nodes = session.GetInputCount();
std::vector<const char*> input_node_names(num_input_nodes);
std::vector<int64_t> input_node_dims;
std::cout << "Number of inputs = " << num_input_nodes << std::endl;
// define shape
const array<int64_t, 4> inputShape = {
1, numChannles, height, width};
const array<int64_t, 2> outputShape = {
1, numClasses};
// define array
array<float, numInputElements> input;
array<float, numClasses> results;
// define Tensor
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
auto inputTensor = Ort::Value::CreateTensor<float>(memory_info, input.data(), input.size(), inputShape.data(), inputShape.size());
auto outputTensor = Ort::Value::CreateTensor<float>(memory_info, results.data(), results.size(), outputShape.data(), outputShape.size());
// copy image data to input array
copy(imageVec.begin(), imageVec.end(), input.begin());
// define names
Ort::AllocatorWithDefaultOptions ort_alloc;
std::vector<const char*> inputNames;
std::vector<const char*> outputNames;
inputNames.push_back(session.GetInputName(0, ort_alloc));
outputNames.push_back(session.GetOutputName(0, ort_alloc));
std::cout << "Input name: " << inputNames[0] << std::endl;
std::cout << "Output name: " << outputNames[0] << std::endl;
// run inference
cout << "run inference. " << endl;
try{
for (size_t i = 0; i < 1; i++)
{
auto start = std::chrono::system_clock::now();
session.Run(Ort::RunOptions{
nullptr}, inputNames.data(), &inputTensor, 1, outputNames.data(), &outputTensor, 1);
auto end = std::chrono::system_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms" << std::endl;
}
}
catch(Ort::Exception& e) {
cout << e.what() << endl;
return 1;
}
// sort results
vector<pair<size_t, float>> indexValuePairs;
cout << "results.size(): " << results[0] << endl;
for(size_t i = 0; i < results.size(); ++i){
cout << "results[i]: " << results[i] << endl;
indexValuePairs.emplace_back(i, results[i]);
}
sort(indexValuePairs.begin(), indexValuePairs.end(), [](const auto& lhs, const auto& rhs)
{
return lhs.second > rhs.second;});
// show Top5
for (size_t i = 0; i < 1; ++i) {
const auto& result = indexValuePairs[i];
cout << i + 1 << ": " << labels[result.first] << " " << result.second << endl;
}
}
helpers.h
#pragma once
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/core.hpp>
using namespace std;
class Helpers{
public:
vector<float> loadImage(const string path, int x, int y);
vector<string> loadLabels(const string path);
};
helpers.cpp
#include <fstream>
#include <iostream>
#include <array>
#include "helpers.h"
using namespace std;
using namespace cv;
const float mean_vals[3] = {
0.3539317f, 0.36820036f, 0.37243055f};
const float scale_vals[3] = {
1.0 / 0.22637626f, 1.0 / 0.22498632f, 1.0 / 0.22197094f};
void normalize_inplace_kxh(cv::Mat &mat_inplace, const float *mean, const float *scale)
{
if (mat_inplace.type() != CV_32FC3) mat_inplace.convertTo(mat_inplace, CV_32FC3);
for (unsigned int i = 0; i < mat_inplace.rows; ++i)
{
cv::Vec3f *p = mat_inplace.ptr<cv::Vec3f>(i);
for (unsigned int j = 0; j < mat_inplace.cols; ++j)
{
p[j][0] = (p[j][0] - mean[0]) * scale[0];
p[j][1] = (p[j][1] - mean[1]) * scale[1];
p[j][2] = (p[j][2] - mean[2]) * scale[2];
}
}
}
vector<float> Helpers::loadImage(const string filename, int sizeX, int sizeY)
{
Mat image = imread(filename);
if (image.empty()) {
cout << "No image found.";
}
// convert from BGR to RGB
cvtColor(image, image, COLOR_BGR2RGB);
// resize
resize(image, image, Size(sizeX, sizeY));
cv::Mat canvas;
image.convertTo(canvas, CV_32FC3, 1. / 255.f, 0.f);
normalize_inplace_kxh(canvas, mean_vals, scale_vals);
// reshape to 1D
canvas = canvas.reshape(1, 1);
// uint_8, [0, 255] -> float, [0, 1]
// Normailze number to between 0 and 1
// Convert to vector<float> from cv::Mat.
vector<float> vec;
canvas.convertTo(vec, CV_32FC1);
// Transpose (Height, Width, Channel)(224,224,3) to (Chanel, Height, Width)(3,224,224)
vector<float> output;
for (size_t ch = 0; ch < 3; ++ch) {
for (size_t i = ch; i < vec.size(); i += 3) {
output.emplace_back(vec[i]);
}
}
return output;
}
vector<string> Helpers::loadLabels(const string filename)
{
vector<string> output;
ifstream file(filename);
if (file) {
string s;
while (getline(file, s)) {
output.emplace_back(s);
}
file.close();
}
return output;
}
スクリプトをコンパイルします。build.sh
mkdir build
cd build
cmake ..
make -j4
スクリプトを実行します。run.sh
data_path=/home/robot/Project/onnx_demo/onnxruntime_resnet-main/assets/1664450262_1664708729_02597_003.jpg
model_path=/home/robot/Project/onnx_demo/onnxruntime_resnet-main/assets/best.onnx
class_path=/home/robot/Project/onnx_demo/onnxruntime_resnet-main/assets/imagenet_classes.txt
./build/demo $model_path $class_path $data_path
5. RK1808の展開
主なプロセスには、
(1) モデルの入力ノード名と出力ノード名の確認、
(2) PC シミュレータ モデルの RKNN への変換とその理由、 (3) C++ のデプロイメント、
(3) C++ のデプロイメント、および
5.1. モデルの入力名と出力名を表示する
Netron を使用して ONNX モデルを開くと、次の図に示すように、[入力、出力] ノードの名前が取得でき、それぞれ [画像、出力0 ] になります。後でモデルを変換するときに、名前を指定する必要があります。入力ノードと出力ノードの。
5.2. RKNN モデルへの変換
import os
import urllib
import traceback
import time
import sys
import numpy as np
import cv2
from rknn.api import RKNN
ONNX_MODEL = 'best.onnx'
RKNN_MODEL = 'best.rknn'
def show_outputs(outputs):
output = outputs[0][0]
output_sorted = sorted(output, reverse=True)
top5_str = 'resnet50v2\n-----TOP 5-----\n'
for i in range(1):
value = output_sorted[i]
index = np.where(output == value)
for j in range(len(index)):
if (i + j) >= 5:
break
if value > 0:
topi = '{}: {}\n'.format(index[j], value)
else:
topi = '-1: 0.0\n'
top5_str += topi
print(top5_str)
if __name__ == '__main__':
# Create RKNN object
rknn = RKNN()
# pre-process config
print('--> Config model')
rknn.config(mean_values=[[90.253, 93.891, 94.97]], std_values=[[57.726, 57.372, 56.603]], reorder_channel='0 1 2')
print('done')
# Load ONNX model
print('--> Loading model')
ret = rknn.load_onnx(model=ONNX_MODEL,
inputs=['images'],
input_size_list=[[3, 256, 256]],
outputs=['output0'])
if ret != 0:
print('Load resnet50v2 failed!')
exit(ret)
print('done')
# Build model
print('--> Building model')
ret = rknn.build(do_quantization=True, dataset='./dataset.txt')
if ret != 0:
print('Build resnet50v2 failed!')
exit(ret)
print('done')
# Export RKNN model
print('--> Export RKNN model')
ret = rknn.export_rknn(RKNN_MODEL)
if ret != 0:
print('Export resnet50v2.rknn failed!')
exit(ret)
print('done')
# Set inputs
img = cv2.imread('./2_256x256.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# init runtime environment
print('--> Init runtime environment')
ret = rknn.init_runtime()
if ret != 0:
print('Init runtime environment failed')
exit(ret)
print('done')
# Inference
print('--> Running model')
t1 = time.time()
outputs = rknn.inference(inputs=[img])
x = outputs[0]
output = np.exp(x)/np.sum(np.exp(x))
outputs = [output]
t2 = time.time()
print('inference time: ', t2-t1)
show_outputs(outputs)
print('done')
rknn.release()
5.3. C++ チップの導入
分類モデルのデプロイについては公式のサンプルを参照すればよく、あまりコードを変更する必要はなく、コンパイルしてそのまま利用することができますが、ここでは詳しく説明しません。
RK ツールチェーン デモ: https://github.com/rockchip-linux/rknpu/tree/master/rknn/rknn_api/examples/rknn_mobilenet_demo
個人的な概要ブログ: Rockchip RV1126 モデルの展開 (完全な展開プロセス)、この例は 1808 A と共有されていますツールチェーンのセットは参照として使用できます
6. チューニング戦略
チューニング戦略についてあまり詳しく説明するのは不便であるため、以下では、データのラベル付け、データ クリーニング、モデルのチューニングなど、いくつかの主要な側面からいくつかの注目すべき問題のみを紹介します。
6.1. データのラベル付け
実際のシーンに直面するとき、基本的なワークフローは次のようになります。
- データ収集: このシナリオでデータ サンプルを取得するには、初期段階で一部を収集してプロセス全体を検証します。将来的には、アルゴリズムの最適化と遭遇する問題に伴い、データ収集に特定の要件が課される可能性もあります。
- デザインのラベル付けルール: たとえば、画像にはどのラベルを付ける必要があるかなど。一部の写真の内容に対応するラベルは曖昧であり、その判断は一意ではない可能性があります。この時点で、何に注目する必要があるでしょうか? トレーニングの焦点は異なりますが、それが写真のラベルを決定する可能性がありますか? したがって、重要なのはタスクをどのように定義するかです。トレーニングの結果とプロジェクトの焦点の違いに応じて、ラベルのラベル付けに影響を与える可能性があります (たとえば、カテゴリ 1 の分類精度により注意を払い、テスト セットの場合はカテゴリの予測に注意を払います) 1 は非常に優れていますが、カテゴリ 2 と 3 は比較的低いです。悪いです。カテゴリ 2 と 3 はほとんどが相互予測誤差であるため、プロジェクトのニーズに応じて許容されます)。
6.2. データのクリーニング
通常、最初にデータ セットにラベルを付ける必要があり、独自のデータ セットの規模に応じて、たとえば、スケールの 10% をトレーニングとテスト用に取り出します。このデータを使用してネットワークをトレーニングし、エフェクトの最初のバージョンを取得します。この時点で、テスト セットについて推論し、テスト セットの予測を確認し、何が間違いで、なぜ間違っているのかを確認します。ネットワーク自体に問題があるのでしょうか、それとも画像自体のラベル定義が非常に曖昧なのでしょうか?
- ダーティ データ クリーニング: ひどく露出している、完全に黒いデータ、ひどくぼやけている、完全に遮られているなど、データの品質が非常に低く、まったく使用できないことを示します。
- ラベルのあいまいな定義: 一部のデータ ラベルは定義が困難です。分類の問題に関する限り、通常は画像のグローバル + ローカル特徴が抽出されます。画像内にターゲット フィーチャと非ターゲット フィーチャの両方が存在する場合があります。カテゴリの定義の問題。
- サンプル分割: 基本モデルのバージョンをトレーニングすると、精度は一定の高さに達します。ただし、テスト セットで推論を行うと、間違って予測されるサンプルが常に存在します。この時点で、トレーニング セットを再度予測し、誤って予測されたサンプルを抽出し、サンプルを 1 つずつチェックして、ラベル自体が間違っていることを確認したほうがよいでしょうか。特徴が明確ではありませんか? 硬いサンプル?現時点では、的を絞った方法で問題を解決してください。
6.3. ネットワークのチューニング
実際のプロジェクトでは、データ クリーニングに重点が置かれる可能性があり、ネットワークの調整は比較的限定的ですが、次の側面を調整できます。具体的なプロセスは次のとおりです。
- ネットワーク構成:導入する機器の性能に応じて、適切なサイズ、幅広い用途、導入が容易なモデルを選択します。
- ハイパーパラメータ: 学習率、バッチサイズなど。
- ネットワークの一般化: 正則化、ドロップアウト、ソフトラベル。