様々な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のスクリプトで実行することを勧めておく。
この一連の流れは同じファイル内に保存するのが望ましい
インストールが終了すると
Do you wish to update your shell profile to automatically initialize conda? This will activate conda on startup and change the command prompt when activated. If you'd prefer that conda's base environment not be activated on startup, run the following command when conda is activated: conda config --set auto_activate_base false You can undo this by running `conda init --reverse $SHELL`? [yes|no]
みたいに出る。
「yes」を選ぶと、condaが自動で初期化され、端末を開いた時点でbase環境がアクティブになります。プロンプト表示 内容も変わります。 「no」を選ぶと、通常はcondaコマンドを手動で有効化(例:conda activate base)する必要があります。
ということなので、パソコン起動時から常駐されたくないから、noを選ぶ。
.bashrcを.bashrc.bakにバックアップを取ってから、末尾に
export PATH=/home/kouzou/anaconda3/bin:$PATH
みたいに書き込む(ユーザー名がkouzouの場合)。 で、bashrcを有効にするために、
$. .bashrc
と打ち込んでから(あるいはターミナルを起動し直して)、condaと打ち込んで
usage: conda [-h] [-v] [--no-plugins] [-V] COMMAND ...
みたいに出ればよい。
ターミナルでanaconda-navigatorと打ち込んで、
Could not initialize GLX 中止 (コアダンプ)
となる場合。
jupyterを使うことが目的の場合は、
jupyter lab jupyter notebook
などで、ブラウザーで使うとうい方法がある。
$ sudo snap install --classic code
codeと打ち込んで起動できればよい。 view→Command Palette...検索欄>にdisplayと打ち込んで、Configure Display Languageを選択→日本語を選択
$ pip install --upgrade pip $ pip install tensorflow
pipはPython用パッケージマネージャー。 インストール終了したら、pythonと打ち込んで、Python対話モードを起動する。 プロンプトが>>>になったら、以下のように入力してみる。 エラーが出ずにバージョンが表示されればよい。
>>> import tensorflow as tf >>> print(tf.__version__)
以下のプログラムで、まずset1でEfficientNetV2S、DenseNet121、MobileNetV3Largeのモデルをそれぞれ計算し、その後にアンサンブル学習を行っています。次に、set2でInceptionResNet、XceptionV2、DenseNet121のモデルをそれぞれ計算し、その後にアンサンブル学習を行っています。このとき、set1でまず計算したDenseNet121の結果(mae_scoreやacc_score)が、set2で計算したDenseNet121の結果(mae_scoreやacc_score)と数パーセントずれるのはなぜですか。
非常に良い質問です。 DenseNet121 を両方のセット(set1・set2)で使っているのに、 MAE や Accuracy が数%ずれるのは ―― スクリプト上で理論的には「同じ条件」に見えても、 実際には完全な再現性が保証されていないためです。
以下に、なぜ差が生じるのかを技術的に解説します。 🧩 差が出る主な原因 ① モデル初期化のランダム性
base = DenseNet121(include_top=False, weights="imagenet", input_tensor=input_img)
weights="imagenet" で初期化する場合、重みは固定されていますが、
上に積んでいる全結合層 (Dense, Dropout, BatchNormalization) は毎回ランダム初期化されます。
➡️ set1 と set2 で 別インスタンスの DenseNet121 を作成しているため、 上位層の重み初期値が異なり、学習曲線がわずかに変化します。 ② Data Augmentation と Soft Labeling の乱数
コード内で次の処理があります👇
score += np.random.uniform(-0.2, 0.2) # Soft Labeling
また、Data Augmentation(ランダム回転・明度変換など)を適用している場合:
tf.image.random_brightness, random_contrast, random_hue ...
➡️ これらのランダム性は、Python・NumPy の seed 固定だけでは完全に抑止できません。 TensorFlow 内部のデータパイプラインが独自に乱数を生成するためです。 ③ 学習過程における GPU / 並列演算の非決定性
TensorFlow + cuDNN(GPU)の場合、 特定のレイヤー(特に BatchNormalization, Dropout)で 計算順序や浮動小数誤差の違いにより、結果が微妙に変わります。
例:同じ初期値でも、計算を別のGPUスレッドで行うと小数第6位以降が変化 → それが累積して MAE/ACC が ±1〜3% 変化することも。
④ 2回目の DenseNet121 の学習が完全に独立
set1 と set2 は以下のように完全に独立して実行されています:
result1 = run_experiment(..., model_set="set1") result2 = run_experiment(..., model_set="set2")
そのため、set2 の DenseNet121 は set1 の学習結果を引き継いでいません。 たとえ構造が同じでも、別々に初期化・最適化・EarlyStopping されているため、 誤差1〜3%のズレはごく自然な範囲です。 🧠 つまり — DenseNet121 の差は「同じモデル構造でも別の学習経路を通った」ため
set1 と set2 の DenseNet121 は パラメータ初期値が異なる別個体。
Soft Labeling と Data Augmentation のランダム性も加わり、 → MAE, Accuracy, F1 が数%ずれる。
✅ 差を最小化したい場合の対策 対策 内容 ① 乱数の完全固定 random.seed, np.random.seed, tf.random.set_seed に加え、 os.environ["TF_DETERMINISTIC_OPS"]="1" を設定(あなたのスクリプトはOK) ② Data Augmentation を停止 ランダム操作をすべて無効化して「同一データ」で学習 ③ Soft Labeling を固定化 np.random.uniform(-0.2, 0.2) をやめるか、固定値に置き換える ④ GPUを無効化 CPU演算は遅いが、決定論的に動作するため再現性が高い ⑤ 同一モデルを再利用 set2でDenseNet121を再インスタンス化せず、set1の学習済みモデルを再評価する 📘 まとめ(Wiki掲載用に適した説明)
DenseNet121 の結果が set1 と set2 で数%異なる理由 同一モデル構造を使用していても、学習の初期化やデータのランダム性により結果が完全には一致しません。 具体的には、①重みの初期化、②Soft LabelingやData Augmentationの乱数、③GPUの並列演算における非決定性などが影響します。 そのため、全く同じ条件で再現性を保証するには、乱数固定に加え、データ拡張を停止し、GPUではなくCPUモードで決定論的演算を行う必要があります。
もし希望があれば、 「このスクリプトで完全に再現性を確保する設定(CPU強制・Augmentation無効・ラベル固定)」 を実装した改良版も書けます。 作成しましょうか? あなた: アンサンブル学習ではなく、DenseNet121のモデルを単独で計算させた場合でも、計算するたびに、こうしたランダム性による影響で毎回、結果が数パーセント変わりますか。あるいは、アンサンブル学習に特有の現象ですか ChatGPT:
非常に鋭い質問です。 結論から言うと――
はい、DenseNet121を単独で計算した場合でも、毎回結果は数%程度変動します。 これはアンサンブル特有の現象ではなく、ニューラルネットワークの学習全般に内在する「非決定性(nondeterminism)」 に起因するものです。
🧩 単独モデル(例:DenseNet121単体)でも結果が変わる主な理由 ① 重み初期値のランダム性
Keras/TensorFlow は各層(Dense, BatchNormなど)の初期重みをランダムで設定します。
x = layers.Dense(512, activation="relu")(x)
たとえ同じデータ・同じエポック数でも、 初期重みがわずかに違えば、最終的に収束する重みが異なるため、 MAEやAccuracyに1〜3%(場合によっては5%)ほど差が出ます。 ② Data Augmentationのランダム性
tf.image.random_brightness, random_flip, random_hue などを使うと、 各エポックで入力データがランダムに変化します。
このため:
同じ画像が「異なる明るさ・彩度」で学習される
結果として学習経路が毎回少しずつ異なる
➡️ 同じDenseNet121でも、毎回微妙に異なる学習パターンになります。 ③ GPU演算の非決定性
TensorFlow + cuDNN (GPU) では以下のような「非決定的な演算」が含まれます。
並列演算の順序が変わる(浮動小数誤差が累積)
BatchNormalization や Dropout の内部処理にランダム性がある
これにより、全く同じコードを2回実行しても結果が異なることがあります。 特に DenseNet は BatchNorm を大量に含むため、影響が顕著です。 ④ EarlyStopping や Validation Split の影響
EarlyStopping もまたランダム分割 (train_test_split) に依存しています。 例えば random_state=42 を固定しても、Augmentation や Dropout のタイミングで validation loss の最小点がずれることがあります。 🧠 対して「アンサンブル学習」ではどうなるか
アンサンブル(複数モデルの平均)はむしろ個々のランダム性を平均化して安定化します。 したがって:
単独モデルよりも結果は安定しやすい
しかし、「同じアンサンブルを2回作る」ときには、やはり個々のモデルの初期値が異なるため、再現性は完全ではない
✅ まとめ(Wiki掲載用にも適した説明)
DenseNet121 単独でも再現性は完全ではない ニューラルネットワークの学習には、初期重み、Data Augmentation、GPU演算、Dropoutなど多くのランダム要素が含まれます。そのため、同一条件でDenseNet121を単独で学習させても、結果(MAEやAccuracyなど)が数パーセント変動することがあります。 一方、アンサンブル学習はこれらのランダム性を平均化して安定させる効果がありますが、アンサンブル全体を再構築する場合も完全な再現性は保証されません。
もし「DenseNet121の結果を完全に再現」したい場合は、 次のような設定を行うとよいです👇
import os, random, numpy as np, tensorflow as tf
os.environ["PYTHONHASHSEED"] = "42" os.environ["TF_DETERMINISTIC_OPS"] = "1" os.environ["TF_CUDNN_DETERMINISTIC"] = "1"
random.seed(42) np.random.seed(42) tf.random.set_seed(42)
# GPUではなくCPUで実行 tf.config.set_visible_devices([], 'GPU')
これでランダム性を最大限抑えられます。 (ただしCPUのみ実行になるため学習時間は大幅に増えます。)
ご希望があれば、「DenseNet121を完全再現性モードで1回ずつ学習・評価する最小スクリプト」を示しますが、作成してみましょうか?