【物体検出】ColabでSSD-Keras（TF1）を動かすためにTensorFlow 2対応した全作業記録

![アイキャッチ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/88c6fd21-282f-4b7d-95b1-4e900eb03968.jpeg)
## はじめに

こんにちは、現在転職活動中の近藤です。

今回の記事では、**Kerasで実装されたSSD（Single Shot MultiBox Detector）をTensorFlow 2環境下のGoogle Colabで動かすまでにやったこと**をまとめてみました。


なぜSSDをTF2対応させたのか？
---


元になったのは以下のリポジトリです👇  
🔗 [rykov8/ssd_keras](https://github.com/rykov8/ssd_keras)

ただ、この実装は **TensorFlow 1.x（+Keras 2.2）** 前提で書かれていて、  
私が使っている **Google Colab（TF2環境）ではそのままでは動かせませんでした。**

GPUパソコンが手元にないため、GPUでモデルを学習するために普段からGoogle Colabを利用しています。ただ、Google Colabの環境ではTensorFlow1系はサポートされておらず、2系で動かす他ありませんでした。
今後の実務やモデル再利用も見据えて、TF2対応に取り組むことにしました。

というわけで、こちらの[一般物体検知アルゴリズム Single Shot MultiBox Detector(SSD)をTensorflow v2で動くようにしつつ理解する](https://techblog.cccmkhd.co.jp/entry/2020/05/11/094834)という記事を参考にさせていただきながら、書き換えを行いました。

---
修正して実行してみると  
> 「あれ？この関数、TF2では使えないのか…？」  
> 「Colab上でこの構文は通らない！」  

など、なかなか細かい詰まりどころが多くて苦戦しました。  


記事の後半では**修正した全てのコードをどのように書き換えたか（Before → After）** 
をマニュアル的にまとめていますので、同じところで詰まった人の助けになれれば幸いです。  

---

今回修正をしたリポジトリはこちらにアップしています👇  
🔧 [tech-kondo/ssd_keras（TF2対応ブランチ）](https://github.com/tech-kondo/ssd_keras/tree/tf2-baseline)  
✅ **TensorFlow 2.18.0** で動作確認済みです！

---

この記事では、以下のような方を想定して書いています：

- TF1ベースの**SSD-KerasをTF2で動かしたい人**
- **Colabで物体検出モデルを学習＆推論させたい人**
- TF1の既存実装を再利用したいけど**エラー地獄にハマってしまった人**


ではさっそくいってみましょう💨！

---

## 🖥️使用環境

今回使用した環境は以下の通りです：

- PC：**Windows11**
- 実行環境：**Google Colab**
- Pythonバージョン：**3.11.12**
- TensorFlowバージョン：**2.18.0**
- Numpyバージョン：**2.0.2**
- Keras：**TensorFlow内蔵版（`tf.keras`）を使用**
- GPU：**ColabProで提供されるGPU（NVIDIA T4）** →無料枠(CPU)でも動きます！

---

今回修正を加えた元コードはこちら👇  
🔗 [rykov8/ssd_keras](https://github.com/rykov8/ssd_keras)

そして私が修正後のコードをアップしたブランチはこちらです👇  
🔧 [tech-kondo/ssd_keras/tree/tf2-baseline](https://github.com/tech-kondo/ssd_keras/tree/tf2-baseline)

データはVOC2007を使用しました。
学習用データ👇
🗂️http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
テストデータ👇
🗂️http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
特徴量抽出器はVGG16モデルベースの重みを使用しました。
🧬weights_SSD300.hdf5👇
https://github.com/kuhung/SSD_keras/releases

用語補足
-**モデル(重み)** は学習した重みのみのデータ
→学習の続きをしたい場合やモデルのカスタム時に使用。
→拡張子は`.h5`や`.hdf5`や`.ckpt`
(ネーミングルールとして`.weights.h5`で保存するがこの場合の拡張子は`.h5`)
-**モデル** はモデルの構造＋重み＋optimizer情報のデータ
→モデルをそのまま配布・デプロイしたいとき、ロードしてすぐ推論したいときに使用。
→拡張子は`.keras`（推奨）か`.h5`

---

### 🔧 修正対象ファイル一覧（全6ファイル）

| ファイル名            | 内容                                              |
|-----------------------|--------------------------------------------------|
| `SSD.ipynb`           | GoogleColabで実行する推論Notebook                  |
| `SSD_training.ipynb`  | GoogleColabで実行する学習Notebook                  |
| `ssd.py`              | 画像から物体を検出するニューラルネットワークを定義    |
| `ssd_layers.py`       | SSD専用のカスタム(特殊)レイヤーを定義               |
| `ssd_training.py`     | 学習設定。損失関数（SmoothL1 / softmax）の実装      |
| `ssd_utils.py`        | 学習前の正解データの整形、推論結果の後処理（NMS含む） |

---
## 🌈 Colab × GitHub × Google Driveで快適に開発できた話

今回の開発では、Google Colabを使って学習・検証を行いました。  
モデルのソースコードはGitHubからForkし、
**作業ファイルやデータはすべてGoogleドライブ上で管理**しています。

---

### 🚀 ローカルからpyファイルが編集できて便利だった！

[パソコン版Googleドライブ（Drive for desktop）](https://support.google.com/a/users/answer/13022292?hl=ja) を使えば、  
Googleドライブ上ファイルを**ローカルのエディタ（VSCodeやsublime textなど）でそのまま編集できてとっても快適でした！**

> ✨ ローカルで編集→そのままGoogleドライブに保存→Colabでそのまま実行！

という流れがすごくスムーズで、かなりストレスなく開発できました。  

### 🚀 データのダウンロードもローカルを介さずに出来て便利だった！  

Google Chromeからだと、**ファイルの保存先としてGoogleドライブ内のフォルダを直接指定**できるため、  `wget` や `curl` を使わずに、ローカルストレージを介さずに、「ダウンロードしたらすぐ学習に使える」状態が整うのでとても快適でした🎈  
❗ただ`wget`でダウンロードしたほうが比にならないほど早いので時間がない方は`wget`の使用をお勧めします。

環境構築や実験の多い機械学習では、こうした **「編集・保存・実行」の動線が快適なこと** がとても大事だと感じました。
  



![エクスプローラーのキャプチャ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/16ad26d4-288b-42da-ac59-7f8f9987b147.jpeg)
こんな感じでエクスプローラーからアクセス出来ます。


---

## 🔧 実際に行ったこと（手順）

1. **GitHub上の元リポジトリをFork**  
　→ [rykov8/ssd_keras](https://github.com/rykov8/ssd_keras) をベースに自分のアカウントに複製

2. **Googleドライブ内にForkしたリポジトリをClone**  
　→ Colabから直接読み込めるように、`/content/drive/MyDrive/プロジェクト名/ssd_keras` 以下に配置

3. **学習・推論に必要なファイルをGoogleドライブ上のプロジェクトファイルに配置**  
　- 学習用のデータ
　- 検証用のテスト画像
　- 修正した `.py` ファイル（`ssd.py`, `ssd_utils.py` など）
　- 学習済みの重みファイル
　- 修正した`SSD_training.ipynb` などのノートブック類

4. `get_data_from_XML.py`または`get_data_from_XML.ipynb`を実行して
データを**学習に扱いやすい形式にまとめたpklファイルを作成**
❗pklファイルの作成時のNumpyのバージョンはモデル学習を実行する環境のバージョンと揃えるよう注意。私は**2.0.2**にしましたが**1.25.2**でも使えます。
5. `SSD_training.ipynb`をいざ実行！（モデルの学習→検証を行います）
6. `SSD.ipynb`で作成したモデルを使用し推論結果を確認🧐

---
## 🔧 具体的な修正箇所
以降のセクションでは、これらのファイルをひとつずつ取り上げて、**修正内容（Before→After）＋補足解説**をしていきます✍️

---

## 🔍 `ssd_layers.py` の修正内容（全9カ所）

以降では、各ファイルごとに**修正箇所（Before→After）＋補足解説**をまとめていきます✍️  
ここでは、`ssd_layers.py` の変更点を **9カ所** に分けて解説します。

---

### 📌 ① モジュールの import 部分の変更

**Before**
```python
from keras.engine.topology import InputSpec
from keras.engine.topology import Layer
```

**After**
```python
from tensorflow.keras.layers import InputSpec
from tensorflow.keras.layers import Layer
```

💡 TF2では `keras` は `tensorflow.keras` を使うのが基本です。

---

### 📌 ② `Normalize` クラスの `__init__` 内

**Before**
```python
if K.image_dim_ordering() == 'tf':
```

**After**
```python
if K.image_data_format() == 'channels_last':
```

💡 TF2系では `image_dim_ordering()` が非推奨。代わりに `image_data_format()` を使用。

---

### 📌 ③ `Normalize` クラスの `build()` メソッド内

**Before**
```python
init_gamma = self.scale * np.ones(shape)
self.gamma = K.variable(init_gamma, name='{}_gamma'.format(self.name))
self.trainable_weights = [self.gamma]
```

**After**
```python
self.gamma = self.add_weight(
    name='{}_gamma'.format(self.name),
    shape=shape,
    initializer=tf.keras.initializers.Constant(self.scale),
    trainable=True
)
```

💡 `add_weight()` を使うことで、Kerasモデルとして正しく学習＆保存できる形式になります。

---

### 📌 ④ `Normalize` クラスの `call()` 内

**Before**
```python
output = K.l2_normalize(x, self.axis)
```

**After**
```python
output = tf.nn.l2_normalize(x, self.axis)
```

💡 TF2では `K` ラッパー経由よりも `tf.nn` を直接使うほうが推奨されています。

---

### 📌 ⑤ `Normalize` クラスに `get_config()` を追加

**After**
```python
def get_config(self):
    config = super().get_config().copy()
    config.update({
        'scale': self.scale,
    })
    return config
```

💡 `get_config()` を追加すると、**モデルの保存・復元時にパラメータを保持できる**ようになります。

---

### 📌 ⑥ `PriorBox` クラスの `__init__` 内

**Before**
```python
if K.image_dim_ordering() == 'tf':
```

**After**
```python
if K.image_data_format() == 'channels_last':
```

💡 ２と同様、データフォーマット取得の方法が変わりました。

---

### 📌 ⑦ `PriorBox` クラスの `call()` 内：shape取得の修正

**Before**
```python
if hasattr(x, '_keras_shape'):
    input_shape = x._keras_shape
elif hasattr(K, 'int_shape'):
    input_shape = K.int_shape(x)
```

**After**
```python
input_shape = x.shape
```

💡 TF2では、`x.shape` で静的なshapeを取得する方法がよりシンプルで推奨されています。

---

### 📌 ⑧ `PriorBox` クラスの `call()` 内：Tensor変換の修正

**Before**
```python
prior_boxes_tensor = K.expand_dims(K.variable(prior_boxes), 0)
```

**After**
```python
prior_boxes_tensor = tf.expand_dims(prior_boxes, 0)
```

💡 Keras backend を使わず、**TensorFlowネイティブな書き方**に移行。

---

### 📌 ⑨ `PriorBox` クラスに `get_config()` を追加

**After**
```python
def get_config(self):
    config = super().get_config().copy()
    config.update({
        'img_size': self.img_size,
        'min_size': self.min_size,
        'max_size': self.max_size,
        'aspect_ratios': self.aspect_ratios,
        'variances': self.variances,
        'clip': self.clip,
    })
    return config
```

💡 モデルの保存・再構築時にレイヤー設定が復元されるようになります。

---

以上が `ssd_layers.py` におけるTF2対応の変更点です！  
それぞれの変更が何のためかも理解しておくと、今後のモデルカスタムにも役立ちます💡

---

## 🔍 `ssd.py` の修正内容（全10カ所）

このセクションでは、`ssd.py` に対して行った **TensorFlow 2 対応の修正点（Before→After）＋補足解説** をまとめます✍️

---

### 📌 ① モジュールの import 部分の修正

**Before**
```python
from keras.layers import Activation
from keras.layers import AtrousConvolution2D
from keras.layers import Convolution2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import GlobalAveragePooling2D
from keras.layers import Input
from keras.layers import MaxPooling2D
from keras.layers import merge
from keras.layers import Reshape
from keras.layers import ZeroPadding2D
from keras.models import Model

from ssd_layers import Normalize
from ssd_layers import PriorBox
```

**After**
```python
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import GlobalAveragePooling2D
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.layers import Concatenate
from tensorflow.keras.layers import Reshape
from tensorflow.keras.layers import ZeroPadding2D
from tensorflow.keras.models import Model

from ssd_layers import Normalize
from ssd_layers import PriorBox
```

💡 TF2では `Conv2D` をはじめとしてすべて `tensorflow.keras.layers` に統一。
また `merge` は `Concatenate` に置き換えられています。

---

### 📌 ② `Convolution2D` → `Conv2D`

**Before**
```python
net['conv1_1'] = Convolution2D(64, 3, 3, ...)
```
**After**
```python
net['conv1_1'] = Conv2D(64, (3, 3), ...)
```

💡 `Conv2D(64, (3, 3))` のようにカーネルサイズはタプルで渡します。

---

### 📌 ③ `border_mode` → `padding`

**Before**
```python
Conv2D(..., border_mode='same')
```
**After**
```python
Conv2D(..., padding='same')
```

💡 `border_mode` は古いKerasの引数で、TF2では `padding` に統一されています。

---

### 📌 ④ `subsample` → `strides`

**Before**
```python
Conv2D(..., subsample=(2, 2))
```
**After**
```python
Conv2D(..., strides=(2, 2))
```

💡 同様に `subsample` も古い表記で、現在は `strides` が正式です。

---

### 📌 ⑤ `atrous_rate` → `dilation_rate`

**Before**
```python
AtrousConvolution2D(..., atrous_rate=(6, 6))
```
**After**
```python
Conv2D(..., dilation_rate=(6, 6))
```

💡 TF2では atrous（空洞）畳み込みも `Conv2D` で処理可能になり、引数が変更されました。

---

### 📌 ⑥ データフォーマット確認方法の修正

**Before**
```python
if K.image_dim_ordering() == 'tf':
```
**After**
```python
if K.image_data_format() == 'channels_last':
```

💡 `image_dim_ordering()` は非推奨になっているため、対応する関数に置き換えています。

---

### 📌 ⑦ `merge` → `Concatenate` に置き換え（loc）

**Before**
```python
net['mbox_loc'] = merge([
    net['conv4_3_norm_mbox_loc_flat'],
    net['fc7_mbox_loc_flat'],
    net['conv6_2_mbox_loc_flat'],
    net['conv7_2_mbox_loc_flat'],
    net['conv8_2_mbox_loc_flat'],
    net['pool6_mbox_loc_flat']
], mode='concat', concat_axis=1, name='mbox_loc')
```

**After**
```python
net['mbox_loc'] = Concatenate(axis=1, name='mbox_loc')([
    net['conv4_3_norm_mbox_loc_flat'],
    net['fc7_mbox_loc_flat'],
    net['conv6_2_mbox_loc_flat'],
    net['conv7_2_mbox_loc_flat'],
    net['conv8_2_mbox_loc_flat'],
    net['pool6_mbox_loc_flat']
])
```

💡 `merge(..., mode='concat')` は TF2では廃止。明示的に `Concatenate()` を使用します。

---

### 📌 ⑧ 同様の `Concatenate` 修正（conf / priorbox / predictions）

💡 `net['mbox_conf']`、`net['mbox_priorbox']`、`net['predictions']` にも同様の修正が必要です。  
全て `merge(..., mode='concat', ...)` を → `Concatenate(...)([...])` に置換してください。

---

### 📌 ⑨ `num_boxes` の shape 取得方法の修正

**Before**
```python
if hasattr(net['mbox_loc'], '_keras_shape'):
    num_boxes = net['mbox_loc']._keras_shape[-1] // 4
elif hasattr(net['mbox_loc'], 'int_shape'):
    num_boxes = K.int_shape(net['mbox_loc'])[-1] // 4
```

**After**
```python
if net['mbox_loc'].shape[-1] is not None:
    num_boxes = net['mbox_loc'].shape[-1] // 4
else:
    num_boxes = tf.shape(net['mbox_loc'])[-1] // 4
```

💡 shapeの取得は `x.shape[-1]` で可能。`None` の場合は `tf.shape()` で動的取得。

---

以上が `ssd.py` における TensorFlow 2 対応の変更点です！
コードの一貫性と再現性を保つためにも、全体を通じて `Keras1系` の記法は `TF2準拠` に統一するのがベストです。

---

## 🔍 `ssd_utils.py` の修正内容（全6カ所）

このセクションでは、`ssd_utils.py` に対して行った **TensorFlow 2 対応の修正点（Before→After）＋補足解説** をまとめます✍️

---

### 📌 ① `BBoxUtility.__init__` の大幅改修

**Before**
```python
self.boxes = tf.placeholder(dtype='float32', shape=(None, 4))
self.scores = tf.placeholder(dtype='float32', shape=(None,))
self.nms = tf.image.non_max_suppression(self.boxes, self.scores,
                                        self._top_k,
                                        iou_threshold=self._nms_thresh)
self.sess = tf.Session(config=tf.ConfigProto(device_count={'GPU': 0}))
```

**After**
```python
def apply_nms(self, boxes, scores):
    """
    Apply Non-Max Suppression on given boxes and scores.

    Args:
        boxes: Tensor of shape (num_boxes, 4)
        scores: Tensor of shape (num_boxes,)

    Returns:
        selected_boxes: Tensor of shape (num_selected, 4)
        selected_scores: Tensor of shape (num_selected,)
    """
    boxes = tf.convert_to_tensor(boxes, dtype=tf.float32)
    scores = tf.convert_to_tensor(scores, dtype=tf.float32)

    nms_indices = tf.image.non_max_suppression(
        boxes,
        scores,
        max_output_size=self._top_k,
        iou_threshold=self._nms_thresh
    )

    selected_boxes = tf.gather(boxes, nms_indices)
    selected_scores = tf.gather(scores, nms_indices)
    return selected_boxes, selected_scores
```

💡 TF2では `placeholder` や `tf.Session()` が使えなくなったため、**NMS処理を関数化**。

---

### 📌 ② `nms_thresh` セッターの修正

**Before**
```python
self.nms = tf.image.non_max_suppression(self.boxes, self.scores,
```

**After**
削除（この行はTF2では不要）

---

### 📌 ③ `top_k` セッターの修正

**Before**
```python
self.nms = tf.image.non_max_suppression(self.boxes, self.scores,
```

**After**
削除（この行も不要）

---

### 📌 ④ `detection_out()` 内：クラス信頼度ゼロ判定の改修

**Before**
```python
if len(c_confs[c_confs_m]) > 0:
    boxes_to_process = decode_bbox[c_confs_m]
    confs_to_process = c_confs[c_confs_m]
```

**After**
```python
if len(c_confs[c_confs_m]) == 0:
    continue
boxes_to_process = decode_bbox[c_confs_m]
confs_to_process = c_confs[c_confs_m]
```

💡 「なにもなければスキップ」の方がロジック的にスマートでバグも減る！

---

### 📌 ⑤ `detection_out()` 内：NMS処理の置き換え

**Before**
```python
feed_dict = {self.boxes: boxes_to_process,
             self.scores: confs_to_process}
idx = self.sess.run(self.nms, feed_dict=feed_dict)
good_boxes = boxes_to_process[idx]
confs = confs_to_process[idx][:, None]
labels = c * np.ones((len(idx), 1))
c_pred = np.concatenate((labels, confs, good_boxes),
                        axis=1)
results[-1].extend(c_pred)
```

**After**
```python
selected_boxes, selected_scores = self.apply_nms(boxes_to_process, confs_to_process)
if selected_boxes.shape[0] == 0:
    continue
confs = tf.expand_dims(selected_scores, axis=-1).numpy()  # shape: (N, 1)
labels = c * np.ones((selected_boxes.shape[0], 1))
c_pred = np.concatenate((labels, confs, selected_boxes.numpy()), axis=1)

results[-1].extend(c_pred)
```

💡 旧式のセッション実行から、**関数形式の `apply_nms()` に分離＆明快化**。

---

### 📌 ⑥ ラベル生成＆連結処理の修正（重複）

**Before**
```python
labels = c * np.ones((len(idx), 1))
c_pred = np.concatenate((labels, confs, good_boxes),
                        axis=1)
results[-1].extend(c_pred)
```

**After**
```python
labels = c * np.ones((selected_boxes.shape[0], 1))
c_pred = np.concatenate((labels, confs, selected_boxes.numpy()), axis=1)
results[-1].extend(c_pred)
```

💡 TF2では `Tensor` は `.numpy()` を使って明示的にNumPyに変換しよう。

---

以上が `ssd_utils.py` における TensorFlow 2 対応の変更点です！

---

## 🔍 `ssd_training.py` の修正内容（全6カ所）

このセクションでは、`ssd_training.py` に対して行った **TensorFlow 2 対応の修正点（Before→After）＋補足解説** をまとめます✍️

---

### 📌 ① `_softmax_loss()` 内：`tf.log` → `tf.math.log`

**Before**
```python
softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                              axis=-1)
```

**After**
```python
softmax_loss = -tf.reduce_sum(y_true * tf.math.log(y_pred),
                              axis=-1)
```

💡 `tf.log()` は非推奨。TF2では `tf.math.log()` を使うのが正式です。

---

### 📌 ② `compute_loss()` 内：`tf.to_float` → `tf.cast(..., tf.float32)`

**Before**
```python
num_boxes = tf.to_float(tf.shape(y_true)[1])
```
**After**
```python
num_boxes = tf.cast(tf.shape(y_true)[1], tf.float32)
```

💡 `tf.to_float()` は削除されたため、明示的に `tf.cast(..., tf.float32)` に変更。

---

### 📌 ③ `has_min` の型変換も同様に修正

**Before**
```python
has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
```
**After**
```python
has_min = tf.cast(tf.reduce_any(pos_num_neg_mask), tf.float32)
```

---

### 📌 ④ `num_neg_batch` の型変換

**Before**
```python
num_neg_batch = tf.to_int32(num_neg_batch)
```
**After**
```python
num_neg_batch = tf.cast(num_neg_batch, tf.int32)
```

💡 `tf.to_int32()` も非推奨。型変換は `tf.cast()` に統一。

---

### 📌 ⑤ `full_indices` の構成部分のキャスト修正

**Before**
```python
full_indices = (tf.reshape(batch_idx, [-1]) * tf.to_int32(num_boxes) +
```

**After**
```python
full_indices = (tf.reshape(batch_idx, [-1]) * tf.cast(num_boxes, tf.int32) +
```

---

### 📌 ⑥ `total_loss` の除算部分もキャスト

**Before**
```python
total_loss /= (num_pos + tf.to_float(num_neg_batch))
```
**After**
```python
total_loss /= (num_pos + tf.cast(num_neg_batch, tf.float32))
```

💡 TensorFlow 2ではすべての型変換処理を `tf.cast()` に統一することが推奨されています。

---

以上が `ssd_training.py` における TensorFlow 2 対応の変更点です！

---

## 🔍 `SSD_training.ipynb` の修正内容（全10カ所）

このセクションでは、ノートブック `SSD_training.ipynb` を TensorFlow 2 向けに対応・整理するために行った変更点をまとめています。**コード修正、importの刷新、学習フローのアップデート、セッション維持の工夫**など、多数の改善が含まれています✍️

---

### 📌 ① セル先頭：依存関係リセット＆インストール用セルの追加

**Before**
記述なし

**After**（新規追加）
```python
# ★★このセルを実行後セッションを再起動してください★★再起動後は実行しなくてOK
# numpy_version=2.0.2 tensorflow_version=2.18.0
!pip uninstall -y keras tensorflow
!pip install tensorflow==2.18.0 --quiet
```

---

### 📌 ② import文の刷新と変数追加

**Before**
```python
import keras
from keras.applications.imagenet_utils import preprocess_input
...
from ssd import SSD300
```

**After**
```python
from tensorflow import keras
from tensorflow.keras.applications.imagenet_utils import preprocess_input
...
from ssd_tf2 import SSD300
```

✅ 追加変数:
```python
threshold = 0.4
path_prefix = # 学習画像を格納したファイル
model_save_path = # モデル(重み)の保存先ファイル
new_weights_name = # 一番最近に保存したモデル(重み)(学習の続きを行いたい場合にのみ必要)
```

---

### 📌 ③ Generator クラス内：画像前処理の変更

**Before**
```python
img = imresize(img, self.image_size).astype('float32')
```

**After**
```python
img = np.array(Image.fromarray((img * 255).astype(np.uint8)).resize(self.image_size)).astype('float32')
```

---

### 📌 ④ Generator末尾：コールバッククラス追加

**After**（新規追加）
```python
class LossLogger(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs=None):
        print(f"Epoch {epoch}: loss={logs.get('loss'):.4f}, val_loss={logs.get('val_loss'):.4f}")
        with open(f'{model_save_path}log/loss_log.txt', 'a') as f:
            f.write(f"{epoch+1},{logs.get('loss'):.4f},{logs.get('val_loss'):.4f}\n")

class FrequentSaver(tf.keras.callbacks.Callback):
    def __init__(self, save_every=100):
        super().__init__()
        self.save_every = save_every
        self.batch_counter = 0

    def on_epoch_begin(self, epoch, logs=None):
        self.current_epoch = epoch

    def on_train_batch_end(self, batch, logs=None):
        self.batch_counter += 1
        if self.batch_counter % self.save_every == 0:
            weights_num = count_ext_files(model_save_path + '100_steps/', ext='.weights.h5')
            filename = f"{model_save_path}100_steps/{weights_num}_epoch-{self.current_epoch:02d}_step-{self.batch_counter:05d}_loss-{logs.get('loss'):.4f}.weights.h5"
            self.model.save_weights(filename)
            print(f'重み保存完了！ → {filename}')
```

---

### 📌 ⑤ optimizer定義：引数名変更

**Before**
```python
optim = keras.optimizers.Adam(lr=base_lr)
```
**After**
```python
optim = keras.optimizers.Adam(learning_rate=base_lr)
```

---

### 📌 ⑥ model.compile に `jit_compile=False` を追加

**Before**
```python
model.compile(optimizer=optim,
              loss=MultiboxLoss(...).compute_loss)
```
**After**
```python
model.compile(optimizer=optim,
              loss=MultiboxLoss(...).compute_loss,
              jit_compile=False)
```

---

### 📌 ⑦ 中断後再開のための重み読み込み行を追加

**After**（新規追加）
```python
model.load_weights(f'{model_save_path}{new_weights_name}.weights.h5')
```

---

### 📌 ⑧ 学習ステップ：`fit_generator` → `fit` に変更

**Before**
```python
model.fit_generator(...)
```
**After**
```python
model.fit(...)
```

---

### 📌 ⑨ 判定しきい値を `0.6` から `threshold` 変数に置換

**Before**
```python
top_indices = [i for i, conf in enumerate(det_conf) if conf >= 0.6]
```
**After**
```python
top_indices = [i for i, conf in enumerate(det_conf) if conf >= threshold]
```

---

### 📌 ⑩ `NUM_CLASSES` に応じた色付け対応

**Before**
```python
colors = plt.cm.hsv(np.linspace(0, 1, 4)).tolist()
```
**After**
```python
colors = plt.cm.hsv(np.linspace(0, 1, NUM_CLASSES)).tolist()
```

---

以上が `ssd_training.py` における TensorFlow 2 対応の変更点です！

---

---

## 🔧 Colab上で開発するために追加した補助機能

ここでは、Google Colab上でSSDモデルを開発・学習する際に追加して便利だったコードスニペットをまとめています。
GPU確認やセッション維持、手動保存機能など、**Colab特有の制約やニーズに合わせた工夫**が詰まっています💡

---

### 🧠【1】GPUの割り当てを確認

```python
# Colab無料枠では "command not found" が出る場合もありますが、問題ありません
gpu_info = !nvidia-smi
gpu_info = '\n'.join(gpu_info)
if gpu_info.find('failed') >= 0:
  print('Not connected to a GPU')
else:
  print(gpu_info)
```

---

### 🗂【2】Google Driveのマウント

```python
from google.colab import drive
drive.mount('/content/drive', force_remount=True)
```

---

### 🔄【3】セッション維持用のバックグラウンド出力

```python
import time
import threading

def keep_alive():
    while True:
        print("Keeping session alive...")
        time.sleep(600)  # 10分間隔で出力

# バックグラウンドスレッドを起動
task = threading.Thread(target=keep_alive, daemon=True)
task.start()
print("バックグラウンドでセッションを維持中...")
```

---

### 🧾【4】モデルのサマリー出力

```python
model.summary()
```

💡 ネットワーク構成やレイヤー名を確認できるので、**デバッグ時や保存ファイル名の命名規則**を考える時にも便利です。

---

### 💾【5】モデルの手動保存コード（必要なときだけコメントアウトを外して使用）

```python
# save_model_name = 'epoch-30_step-0000_loss-1.9178_valloss-2.9057' # ←手動で編集！
# weights_num = count_ext_files(model_save_path, ext='.weights.h5')
# model_num = count_ext_files(model_save_path, ext='.keras')
# model.save_weights(f'{model_save_path}{weights_num}_{save_model_name}.weights.h5')
# model.save(f'{model_save_path}{model_num}_{save_model_name}.keras', include_optimizer=True)
```

---

### 📈【6】損失の可視化（学習曲線）

```python
plt.plot(history.history['loss'], label='loss')
plt.plot(history.history['val_loss'], label='val_loss')
plt.legend()
plt.title("Training Loss vs Validation Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.grid(True)
plt.show()
```

---

これらの追加機能を使うことで、Colab上でも**より安定かつ柔軟に開発・検証・保存が可能**になります！
「やりたいことに集中できる」環境を作ってくれる大事なパーツたちです✨

---

## ✅ 最後に：TF2対応 修正まとめ表【Before→After 対応一覧】

これまでに行った `SSD-Keras` → `TensorFlow 2 対応` に向けた書き換え作業を、**ファイル別 × 対応項目ごとに一括で整理**しました。記事やリファレンスとしても使える一覧です🛠✨

| 修正箇所 | Before | After |
|-----------|--------|-------|
| import 文 | keras.engine.topology | tensorflow.keras.layers |
| 画像フォーマット確認関数 | `K.image_dim_ordering()` | `K.image_data_format()` |
| 重みの定義 | `K.variable(...)` | `self.add_weight(...)` |
| 正規化処理関数 | `K.l2_normalize` | `tf.nn.l2_normalize` |
| get_config未定義 | なし | `get_config()` を追加 |
| 畳み込み層 | `Convolution2D` | `Conv2D` + タプル形式 |
| 引数名称 | `border_mode='same'` | `padding='same'` |
| レイヤー結合 | `merge(..., mode='concat')` | `Concatenate(...)` |
| shapeの取得 | `._keras_shape` | `.shape` or `tf.shape()` |
| placeholderの使用 | `tf.placeholder`, `tf.Session()` | 関数 `apply_nms()` に分離 |
| NMS処理 | `sess.run(self.nms)` | `apply_nms()` 呼び出し |
| tf系型変換関数 | `tf.to_float`, `tf.to_int32` | `tf.cast(..., dtype)` |
| 対数関数 | `tf.log()` | `tf.math.log()` |
| import 文 | `import keras` | `from tensorflow import keras` |
| fit_generatorの記法 | `fit_generator(...)` | `fit(...)` |
| 画像リサイズ処理 | `imresize()` | `Image.resize()` via PIL |

---

このまとめ表が**既存のTF1系KerasコードをTF2へ移行したい方の参考資料**にもなればうれしいです。

---

## 🧐 学習＆推論結果を確認
### 📉 学習の実行
SSD_training.ipynbをcolabで実行
![ssd_tf2_train_start.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/332943a0-eafa-4fb7-871a-1d41d05f569f.jpeg)
うまく学習が開始されました。
　**GoogleColabのTesla T4を使用して30epochで17時間かかりました。**
### 📉 学習中のLoss推移（30エポック）
学習初期で一気に収束しました。 
![ssd_tf2_train_loss_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/14972398-3fa3-4024-9634-142e4d8970f5.jpeg)

lossはおおよそ2.5 → 1.9まで改善が見られました。それ以降は見ての通り停滞しています。
val_lossに関しては2.95 → 2.90あたりでずっと横ばいでした。
もう少し早く切り上げてもよかったなと思います。
汎化出来ていなさそうな感じがします。（学習が浅いのか？）

### 🔍精度検証
-モデル：val_loss の最小値のepoch13のモデルで精度検証を行います。
-検証データ：VOC2007test
-クラス：'Aeroplane', 'Bicycle', 'Bird', 'Boat', 'Bottle',
               'Bus', 'Car', 'Cat', 'Chair', 'Cow', 'Diningtable',
               'Dog', 'Horse','Motorbike', 'Person', 'Pottedplant',
               'Sheep', 'Sofa', 'Train', 'Tvmonitor'の20クラス
               
**🔧閾値調整**
-NMSのIoU閾値
既存の閾値では重複検出が多かったので、ssd_utils_tf2.pyのBBoxUtilityクラスへの引数にあるnms_threshを0.45から0.3へ引き下げました。　これによって予測ボックス同士のIoUが nms_thresh以上のものは、信頼度の高い方だけを残して他は捨てるという調整をしています。
-判定結果の確信度の閾値
またモデル判定の閾値も0.6に設定しました。
これにより確信度0.6以上のものだけを判定結果として表示するようにしています。

**ここからは実際の推論結果をご覧ください！**
うまく判定できたもの
![01_model_good_prediction.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/346c0b49-b231-4d53-81a2-8f0ec7335c2a.jpeg)
うまく判定出来なかったもの
![01_model_bad_prediction.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046428/9c141957-40d8-4ef2-ace0-48743399fd82.jpeg)

目視で確認したところ精度は40~50%くらいでした。

### 🔥改善案
#### 1.VOC2012の学習データを追加
→SSDの論文ではVOC2007だけでなく、VOC2012も学習データとして使用していたので、同じようにVOC2012の学習データを追加する。
#### 2.クラスごとの学習枚数を調整
→クラスによって精度の偏りが大きい。学習データの量が少なければaugmentationで水増ししてバランスを調整する。
#### 3.learning_rateの調整
→途中で学習率を下げて後半でうまく収束させる

👇
上記の精度改善に取り組みたいところですが
GoogleColabのGPU使用制限が来てしまったため、今回はここで断念。

---
## ✍️ おわりに：得られた学び・今後の展望

今回のTensorFlow1系Kerasで実装されたコードの書き換え作業を通して、ひとつひとつの処理の理解を深めることが出来ました。想定以上に修正箇所が多かったので作業は難航しましたが無事に学習とテストをcolab環境で実行できました。

この機会にTensorFlow2では動かない書き方をストック出来たので
今後TensorFlow1系のプログラムを修正するときには重宝しそうです🌱
今後の展望としては学習の一部の処理を低レベルAPIに書き換え、
**TensorFlowでのカスタム性の高い学習を実現したい**です！！🔥🔥

参考になった方は、**いいね・ストック・フォロー**してもらえると嬉しいです！