|
a |
|
b/YOLO/train_bottleneck.py |
|
|
1 |
""" |
|
|
2 |
Retrain the YOLO model for your own dataset. |
|
|
3 |
""" |
|
|
4 |
import os |
|
|
5 |
import numpy as np |
|
|
6 |
import keras.backend as K |
|
|
7 |
from keras.layers import Input, Lambda |
|
|
8 |
from keras.models import Model |
|
|
9 |
from keras.optimizers import Adam |
|
|
10 |
from keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping |
|
|
11 |
|
|
|
12 |
from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss |
|
|
13 |
from yolo3.utils import get_random_data |
|
|
14 |
|
|
|
15 |
|
|
|
16 |
def _main(): |
|
|
17 |
annotation_path = 'train.txt' |
|
|
18 |
log_dir = 'logs/000/' |
|
|
19 |
classes_path = 'model_data/coco_classes.txt' |
|
|
20 |
anchors_path = 'model_data/yolo_anchors.txt' |
|
|
21 |
class_names = get_classes(classes_path) |
|
|
22 |
num_classes = len(class_names) |
|
|
23 |
anchors = get_anchors(anchors_path) |
|
|
24 |
|
|
|
25 |
input_shape = (416,416) # multiple of 32, hw |
|
|
26 |
|
|
|
27 |
model, bottleneck_model, last_layer_model = create_model(input_shape, anchors, num_classes, |
|
|
28 |
freeze_body=2, weights_path='model_data/yolo_weights.h5') # make sure you know what you freeze |
|
|
29 |
|
|
|
30 |
logging = TensorBoard(log_dir=log_dir) |
|
|
31 |
checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5', |
|
|
32 |
monitor='val_loss', save_weights_only=True, save_best_only=True, period=3) |
|
|
33 |
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1) |
|
|
34 |
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1) |
|
|
35 |
|
|
|
36 |
val_split = 0.1 |
|
|
37 |
with open(annotation_path) as f: |
|
|
38 |
lines = f.readlines() |
|
|
39 |
np.random.seed(10101) |
|
|
40 |
np.random.shuffle(lines) |
|
|
41 |
np.random.seed(None) |
|
|
42 |
num_val = int(len(lines)*val_split) |
|
|
43 |
num_train = len(lines) - num_val |
|
|
44 |
|
|
|
45 |
# Train with frozen layers first, to get a stable loss. |
|
|
46 |
# Adjust num epochs to your dataset. This step is enough to obtain a not bad model. |
|
|
47 |
if True: |
|
|
48 |
# perform bottleneck training |
|
|
49 |
if not os.path.isfile("bottlenecks.npz"): |
|
|
50 |
print("calculating bottlenecks") |
|
|
51 |
batch_size=8 |
|
|
52 |
bottlenecks=bottleneck_model.predict_generator(data_generator_wrapper(lines, batch_size, input_shape, anchors, num_classes, random=False, verbose=True), |
|
|
53 |
steps=(len(lines)//batch_size)+1, max_queue_size=1) |
|
|
54 |
np.savez("bottlenecks.npz", bot0=bottlenecks[0], bot1=bottlenecks[1], bot2=bottlenecks[2]) |
|
|
55 |
|
|
|
56 |
# load bottleneck features from file |
|
|
57 |
dict_bot=np.load("bottlenecks.npz") |
|
|
58 |
bottlenecks_train=[dict_bot["bot0"][:num_train], dict_bot["bot1"][:num_train], dict_bot["bot2"][:num_train]] |
|
|
59 |
bottlenecks_val=[dict_bot["bot0"][num_train:], dict_bot["bot1"][num_train:], dict_bot["bot2"][num_train:]] |
|
|
60 |
|
|
|
61 |
# train last layers with fixed bottleneck features |
|
|
62 |
batch_size=8 |
|
|
63 |
print("Training last layers with bottleneck features") |
|
|
64 |
print('with {} samples, val on {} samples and batch size {}.'.format(num_train, num_val, batch_size)) |
|
|
65 |
last_layer_model.compile(optimizer='adam', loss={'yolo_loss': lambda y_true, y_pred: y_pred}) |
|
|
66 |
last_layer_model.fit_generator(bottleneck_generator(lines[:num_train], batch_size, input_shape, anchors, num_classes, bottlenecks_train), |
|
|
67 |
steps_per_epoch=max(1, num_train//batch_size), |
|
|
68 |
validation_data=bottleneck_generator(lines[num_train:], batch_size, input_shape, anchors, num_classes, bottlenecks_val), |
|
|
69 |
validation_steps=max(1, num_val//batch_size), |
|
|
70 |
epochs=30, |
|
|
71 |
initial_epoch=0, max_queue_size=1) |
|
|
72 |
model.save_weights(log_dir + 'trained_weights_stage_0.h5') |
|
|
73 |
|
|
|
74 |
# train last layers with random augmented data |
|
|
75 |
model.compile(optimizer=Adam(lr=1e-3), loss={ |
|
|
76 |
# use custom yolo_loss Lambda layer. |
|
|
77 |
'yolo_loss': lambda y_true, y_pred: y_pred}) |
|
|
78 |
batch_size = 16 |
|
|
79 |
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size)) |
|
|
80 |
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes), |
|
|
81 |
steps_per_epoch=max(1, num_train//batch_size), |
|
|
82 |
validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes), |
|
|
83 |
validation_steps=max(1, num_val//batch_size), |
|
|
84 |
epochs=50, |
|
|
85 |
initial_epoch=0, |
|
|
86 |
callbacks=[logging, checkpoint]) |
|
|
87 |
model.save_weights(log_dir + 'trained_weights_stage_1.h5') |
|
|
88 |
|
|
|
89 |
# Unfreeze and continue training, to fine-tune. |
|
|
90 |
# Train longer if the result is not good. |
|
|
91 |
if True: |
|
|
92 |
for i in range(len(model.layers)): |
|
|
93 |
model.layers[i].trainable = True |
|
|
94 |
model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred}) # recompile to apply the change |
|
|
95 |
print('Unfreeze all of the layers.') |
|
|
96 |
|
|
|
97 |
batch_size = 4 # note that more GPU memory is required after unfreezing the body |
|
|
98 |
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size)) |
|
|
99 |
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes), |
|
|
100 |
steps_per_epoch=max(1, num_train//batch_size), |
|
|
101 |
validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes), |
|
|
102 |
validation_steps=max(1, num_val//batch_size), |
|
|
103 |
epochs=100, |
|
|
104 |
initial_epoch=50, |
|
|
105 |
callbacks=[logging, checkpoint, reduce_lr, early_stopping]) |
|
|
106 |
model.save_weights(log_dir + 'trained_weights_final.h5') |
|
|
107 |
|
|
|
108 |
# Further training if needed. |
|
|
109 |
|
|
|
110 |
|
|
|
111 |
def get_classes(classes_path): |
|
|
112 |
'''loads the classes''' |
|
|
113 |
with open(classes_path) as f: |
|
|
114 |
class_names = f.readlines() |
|
|
115 |
class_names = [c.strip() for c in class_names] |
|
|
116 |
return class_names |
|
|
117 |
|
|
|
118 |
def get_anchors(anchors_path): |
|
|
119 |
'''loads the anchors from a file''' |
|
|
120 |
with open(anchors_path) as f: |
|
|
121 |
anchors = f.readline() |
|
|
122 |
anchors = [float(x) for x in anchors.split(',')] |
|
|
123 |
return np.array(anchors).reshape(-1, 2) |
|
|
124 |
|
|
|
125 |
|
|
|
126 |
def create_model(input_shape, anchors, num_classes, load_pretrained=True, freeze_body=2, |
|
|
127 |
weights_path='model_data/yolo_weights.h5'): |
|
|
128 |
'''create the training model''' |
|
|
129 |
K.clear_session() # get a new session |
|
|
130 |
image_input = Input(shape=(None, None, 3)) |
|
|
131 |
h, w = input_shape |
|
|
132 |
num_anchors = len(anchors) |
|
|
133 |
|
|
|
134 |
y_true = [Input(shape=(h//{0:32, 1:16, 2:8}[l], w//{0:32, 1:16, 2:8}[l], \ |
|
|
135 |
num_anchors//3, num_classes+5)) for l in range(3)] |
|
|
136 |
|
|
|
137 |
model_body = yolo_body(image_input, num_anchors//3, num_classes) |
|
|
138 |
print('Create YOLOv3 model with {} anchors and {} classes.'.format(num_anchors, num_classes)) |
|
|
139 |
|
|
|
140 |
if load_pretrained: |
|
|
141 |
model_body.load_weights(weights_path, by_name=True, skip_mismatch=True) |
|
|
142 |
print('Load weights {}.'.format(weights_path)) |
|
|
143 |
if freeze_body in [1, 2]: |
|
|
144 |
# Freeze darknet53 body or freeze all but 3 output layers. |
|
|
145 |
num = (185, len(model_body.layers)-3)[freeze_body-1] |
|
|
146 |
for i in range(num): model_body.layers[i].trainable = False |
|
|
147 |
print('Freeze the first {} layers of total {} layers.'.format(num, len(model_body.layers))) |
|
|
148 |
|
|
|
149 |
# get output of second last layers and create bottleneck model of it |
|
|
150 |
out1=model_body.layers[246].output |
|
|
151 |
out2=model_body.layers[247].output |
|
|
152 |
out3=model_body.layers[248].output |
|
|
153 |
bottleneck_model = Model([model_body.input, *y_true], [out1, out2, out3]) |
|
|
154 |
|
|
|
155 |
# create last layer model of last layers from yolo model |
|
|
156 |
in0 = Input(shape=bottleneck_model.output[0].shape[1:].as_list()) |
|
|
157 |
in1 = Input(shape=bottleneck_model.output[1].shape[1:].as_list()) |
|
|
158 |
in2 = Input(shape=bottleneck_model.output[2].shape[1:].as_list()) |
|
|
159 |
last_out0=model_body.layers[249](in0) |
|
|
160 |
last_out1=model_body.layers[250](in1) |
|
|
161 |
last_out2=model_body.layers[251](in2) |
|
|
162 |
model_last=Model(inputs=[in0, in1, in2], outputs=[last_out0, last_out1, last_out2]) |
|
|
163 |
model_loss_last =Lambda(yolo_loss, output_shape=(1,), name='yolo_loss', |
|
|
164 |
arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})( |
|
|
165 |
[*model_last.output, *y_true]) |
|
|
166 |
last_layer_model = Model([in0,in1,in2, *y_true], model_loss_last) |
|
|
167 |
|
|
|
168 |
|
|
|
169 |
model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss', |
|
|
170 |
arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})( |
|
|
171 |
[*model_body.output, *y_true]) |
|
|
172 |
model = Model([model_body.input, *y_true], model_loss) |
|
|
173 |
|
|
|
174 |
return model, bottleneck_model, last_layer_model |
|
|
175 |
|
|
|
176 |
def data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes, random=True, verbose=False): |
|
|
177 |
'''data generator for fit_generator''' |
|
|
178 |
n = len(annotation_lines) |
|
|
179 |
i = 0 |
|
|
180 |
while True: |
|
|
181 |
image_data = [] |
|
|
182 |
box_data = [] |
|
|
183 |
for b in range(batch_size): |
|
|
184 |
if i==0 and random: |
|
|
185 |
np.random.shuffle(annotation_lines) |
|
|
186 |
image, box = get_random_data(annotation_lines[i], input_shape, random=random) |
|
|
187 |
image_data.append(image) |
|
|
188 |
box_data.append(box) |
|
|
189 |
i = (i+1) % n |
|
|
190 |
image_data = np.array(image_data) |
|
|
191 |
if verbose: |
|
|
192 |
print("Progress: ",i,"/",n) |
|
|
193 |
box_data = np.array(box_data) |
|
|
194 |
y_true = preprocess_true_boxes(box_data, input_shape, anchors, num_classes) |
|
|
195 |
yield [image_data, *y_true], np.zeros(batch_size) |
|
|
196 |
|
|
|
197 |
def data_generator_wrapper(annotation_lines, batch_size, input_shape, anchors, num_classes, random=True, verbose=False): |
|
|
198 |
n = len(annotation_lines) |
|
|
199 |
if n==0 or batch_size<=0: return None |
|
|
200 |
return data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes, random, verbose) |
|
|
201 |
|
|
|
202 |
def bottleneck_generator(annotation_lines, batch_size, input_shape, anchors, num_classes, bottlenecks): |
|
|
203 |
n = len(annotation_lines) |
|
|
204 |
i = 0 |
|
|
205 |
while True: |
|
|
206 |
box_data = [] |
|
|
207 |
b0=np.zeros((batch_size,bottlenecks[0].shape[1],bottlenecks[0].shape[2],bottlenecks[0].shape[3])) |
|
|
208 |
b1=np.zeros((batch_size,bottlenecks[1].shape[1],bottlenecks[1].shape[2],bottlenecks[1].shape[3])) |
|
|
209 |
b2=np.zeros((batch_size,bottlenecks[2].shape[1],bottlenecks[2].shape[2],bottlenecks[2].shape[3])) |
|
|
210 |
for b in range(batch_size): |
|
|
211 |
_, box = get_random_data(annotation_lines[i], input_shape, random=False, proc_img=False) |
|
|
212 |
box_data.append(box) |
|
|
213 |
b0[b]=bottlenecks[0][i] |
|
|
214 |
b1[b]=bottlenecks[1][i] |
|
|
215 |
b2[b]=bottlenecks[2][i] |
|
|
216 |
i = (i+1) % n |
|
|
217 |
box_data = np.array(box_data) |
|
|
218 |
y_true = preprocess_true_boxes(box_data, input_shape, anchors, num_classes) |
|
|
219 |
yield [b0, b1, b2, *y_true], np.zeros(batch_size) |
|
|
220 |
|
|
|
221 |
if __name__ == '__main__': |
|
|
222 |
_main() |