様々なCNNモデル(の組み合わせ4パターンは以下に記載)を試したが学習時間・精度ともに元々学習に使っていた"EfficientNetV2S", "DenseNet121", "MobileNetV3Large" の組み合わせが1番であったためこれから当面の間はMMLI.py と CNN+binning3.pyで錆画像の判別を行っていくこととする。
def load_images_and_scores(folder): #指定フォルダから画像とスコアを読み込む関数を定義
images, scores = [], [] #画像データとスコアを格納するリストを初期化
for fname in sorted(os.listdir(folder)): #フォルダ内のファイル名を並び替えて1つずつ処理
if fname.endswith(".png") or fname.endswith(".jpg"): #画像ファイル(png/jpg)のみ処理対象
try: #エラー処理のためのtryブロック
score = float(fname[0]) #ファイル名の先頭文字からスコア(ラベル)を取得(例:「3_xxx.png」→3.0)
score += np.random.uniform(-0.2, 0.2) #スコアにランダムなノイズを任意で設定しラベルをソフト化
if score < 1.0 or score > 5.0: #1.00点未満または5.01点以上の場合は学習に再利用・結果に保存しない
print(f"⚠️ スコア範囲外: {fname} → {score:.2f} → スキップ")
continue
path = os.path.join(folder, fname) #画像ファイルのフルパスを作成
img = Image.open(path).convert("RGB").resize(IMAGE_SIZE) #画像を開いてRGBに変換し、指定サイズにリサイズ
images.append(np.array(img) / 255.0) #画像をNumPy配列に変換し、0〜1に正規化してリストに追加
scores.append(score) #スコアをリストに追加
except: #例外が発生した場合の処理
print(f"スキップ: {fname}")
return np.array(images), np.array(scores) #画像とスコアのリストをNumPy配列に変換して返す def create_model(version): #指定したバージョン番号(インデックス)に対応するモデルを構築する関数の定義
input_img = Input(shape=(*IMAGE_SIZE, 3)) #入力画像の形状(IMAGE_SIZE, 3チャンネル)でKerasのInput層を作成
if version == 0:
base = EfficientNetV2S(include_top=False, weights="imagenet", input_tensor=input_img) #モデル名がEfficientNetV2Sの場合の分岐
elif version == 1:
base = DenseNet121(include_top=False, weights="imagenet", input_tensor=input_img)
elif version == 2:
base = MobileNetV3Large(include_top=False, weights="imagenet", input_tensor=input_img)
base.trainable = False #ベースモデルの重みを凍結し転移学習で特徴抽出器として使用
x = base.output #ベースモデルの出力をxに代入
x = layers.GlobalAveragePooling2D()(x) #グローバル平均プーリング層で特徴マップを1次元ベクトルに変換
x = layers.Dense(2048, activation='relu')(x) #全結合層(2048ユニット、ReLU活性化)を追加
x = layers.BatchNormalization()(x) #バッチ正規化層を追加し学習を安定化
x = layers.Dropout(0.5)(x) #ドロップアウト(50%)で過学習を防止
.
.
.
output = layers.Dense(1)(x) #出力層(1ユニット、活性化なし=線形)を追加
output = ClipLayer()(output) #出力値を1.0〜5.0にクリップするカスタムレイヤー(ClipLayer)を適用
model = Model(inputs=input_img, outputs=output) #入力と出力を指定してKerasのModelを構築
model.compile(optimizer='adam', loss=custom_penalizing_loss, metrics=['mae']) #モデルをAdam最適化, カスタム損失関数, MAE評価指標でコンパイル
return model #構築したモデルを返す # === binning: 回帰スコア → 評点1〜5 ===
→回帰モデルの連続値出力を1〜5の整数評価に変換する関数を定義
def binning(score): #binning関数の定義
if score < 2.3: return 1
elif score < 3.1: return 2
elif score < 3.4: return 3
elif score < 3.7: return 4
else: return 5
# === モデル学習 ===
→データを読み込み、訓練・検証セットに分割し、全ての選択モデルについて構築・学習・保存を行う
x, y = load_images_and_scores(TRAIN_FOLDER) #学習用画像とスコアを読み込み
x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.2, random_state=42) #データセットを訓練用と検証用(test_size)に8:2で分割
model = create_model(i) #モデルを構築
.
.
.
model.save(MODEL_PATHS[i]) #学習済みモデルを保存
print(f"✅ モデル{i+1}保存完了: {MODEL_PATHS[i]}"# === 評点予測・保存 ===
→アンサンブル学習済みモデルでテスト画像を予測し、平均スコアを整数評点に変換、評点ごとに画像を保存。予測過程を出力
models_ensemble = [ #アンサンブル用に複数の学習済みモデルをリストとして読み込み
tf.keras.models.load_model(p, compile=False) #各モデルファイル(パスp)を、カスタムレイヤーClipLayerを認識できるようにして読み込み
for p in MODEL_PATHS #上記の読み込みを、全てのモデルファイルに対して実行
]
test_fnames = sorted([f for f in os.listdir(TEST_FOLDER) if f.endswith('.png') or f.endswith('.jpg')]) #テスト用フォルダ内の画像ファイル名(png/jpg)を取得して並び替え
os.makedirs(OUTPUT_BASE, exist_ok=True) #出力用のベースフォルダを作成(既に存在してもエラーにならない)
for i in range(1, 6): #評点1〜5の各スコア用サブフォルダを作成するためのループ
os.makedirs(os.path.join(OUTPUT_BASE, f"score{i}"), exist_ok=True) #各スコアごとの出力サブフォルダを作成
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
for fname in test_fnames: #テスト画像ごとにループ
path = os.path.join(TEST_FOLDER, fname) #画像ファイルのフルパスを作成
img = Image.open(path).convert("RGB").resize(IMAGE_SIZE) #画像を開き、RGB化・リサイズ
x_input = np.array(img) / 255.0 #画像をNumPy配列に変換し正規化
x_input = x_input.reshape(1, *IMAGE_SIZE, 3) #モデル入力用に配列の形状を調整(バッチ次元追加)
preds = [model.predict(x_input, verbose=0)[0][0] for model in models_ensemble] #各モデルで画像を予測しスコアをリスト化
avg_score = np.mean(preds) #各モデルの予測スコアの平均値を計算します(アンサンブル平均)
final_score = binning(avg_score) #平均スコアを1〜5の整数評点に変換
save_path = os.path.join(OUTPUT_BASE, f"score{final_score}", fname) #評点ごとのサブフォルダに保存するためのパスを作成
img.save(save_path) #画像を該当サブフォルダに保存
print(f"📂 {fname} → 回帰スコア: {avg_score:.2f} → 評点: {final_score}") #画像名・回帰スコア・最終評点を表示
# === 検証評価 ===
→検証データでアンサンブル予測を行い、平均スコアを整数評点に変換して正解と比較できる形に整形する
val_preds = [model.predict(x_val, verbose=0).flatten() for model in models_ensemble] #検証データに対して各モデルで予測し、スコア配列をリスト化
y_val_pred = np.mean(val_preds, axis=0) #各サンプルについて全モデルのスコア平均を計算
y_val_bin = np.array([binning(s) for s in y_val_pred]) #平均スコアを1〜5の整数評点に変換
y_true_bin = np.array([binning(s) for s in y_val]) #正解スコアも同様に整数評点に変換| 特徴 / スクリプト名 | CNN+binning.py | CNN+binning2.py | CNN+binning3.py | CNN+binning4.py |
| モデルの種類 | オリジナルCNN3種 | 強化CNN(層数増加) | EfficientNetV2S / DenseNet121 / MobileNetV3 | 同左 |
| アンサンブル学習 | ○ | ○ | ○ | ○ |
| 出力形式 | 回帰+binning | 回帰+binning | 回帰+binning | 回帰+Sigmoid正規化+binning |
| Soft Labeling(評点ぼかし) | ○ | ○ | ○ | ○ |
| 損失関数 | MSE+重みペナルティ | 強化ペナルティ損失関数 | 同左 | 同左 |
| 特徴量の利用 | × | × | × | ○(画像+数値特徴:面積、粒径、Sobel量) |
| 特徴量融合方法 | ー | ー | ー | CNN出力ベクトル+数値特徴 → Dense結合 |
| 出力スケーリング | × | × | × | ○(シグモイド関数+0〜5.0スケーリング) |
| カスタムbinning関数 | ○ | ○ | ○ | ○ |
| UMAP可視化 | ○ | ○ | ○ | ○ |
| 判別補正(3⇔4の補正など) | × | × | × | ○(特徴量条件付き補正が可能な構造) |
| 使用層の深さ・複雑さ | 普通 | やや深め | 深層+転移学習モデル | 同左 |
| 項目 | モデル1 | モデル2 | モデル3 |
| 畳み込み層 | 1層 | 2層 | 2層 |
| プーリング | 1回 | 1回 | 1回 |
| 正規化 | × | 〇 | 〇 |
| Dropout | × | 〇(1回) | 〇(2回) |
| Dense層 | 64ユニット | 128ユニット | 64ユニット |
| 出力層 | softmax(10) | softmax(10) | softmax(10) |
| 学習の安定性 | 弱い(過学習しやすい) | 普通(汎化性能〇) | 強い(深く・安定) |
ただVer5.0は様々な学習要素を詰め込んでしまったのが原因なのか判別精度がかえって落ちてしまったため、今後手書き数字の画像判別を行なう際にはVer4.0のスクリプトで実行することを勧めておく。
この一連の流れは同じファイル内に保存するのが望ましい