Revive old code.
This commit is contained in:
commit
9a79d16293
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
data/
|
||||
img/
|
||||
**/__pycache__/
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
data/
|
||||
img/
|
||||
**/__pycache__/
|
||||
*.swp
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM gw000/keras:2.0.8-py3-tf-cpu
|
||||
|
||||
RUN pip3 install scikit-image sklearn
|
||||
|
||||
RUN mkdir /data
|
||||
ADD icenet/ /tmp/icenet
|
||||
ADD run_model.py /tmp/run_model.py
|
||||
WORKDIR /tmp
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENTRYPOINT ["python3", "run_model.py"]
|
||||
62
README.md
Normal file
62
README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Ship vs iceberg discriminator
|
||||
|
||||
TL;DR: Discriminate between ships and icebergs from SAR imagery.
|
||||
|
||||
|
||||
## Approach
|
||||
|
||||
Data augmentation and parameter sharing. CNN and ResNets.
|
||||
|
||||
|
||||
## Data directory
|
||||
|
||||
The data directory is expected to have the structure as shown below:
|
||||
|
||||
data/
|
||||
├── params
|
||||
│ ├── base_cnn-scaling.pkl
|
||||
│ ├── base_cnn-weights-loss.h5
|
||||
│ ├── base_cnn-weights-val_loss.h5
|
||||
│ ├── icenet-weights-loss.h5
|
||||
│ └── icenet-weights-val_loss.h5
|
||||
├── predictions
|
||||
│ └── icenet-dev.csv
|
||||
├── sample_submission.csv
|
||||
├── test.json
|
||||
└── train.json
|
||||
|
||||
where `{train,test}.json` is the data from the
|
||||
[kaggle website](https://www.kaggle.com/c/statoil-iceberg-classifier-challenge).
|
||||
|
||||
|
||||
## Log
|
||||
|
||||
### Residual base CNN
|
||||
|
||||
Summary:
|
||||
|
||||
* Test loss: 0.5099
|
||||
* Test accuracy: 0.7932
|
||||
* Epochs: 100
|
||||
* Best val loss at epoch 70 (converged until 100, did not overfit)
|
||||
|
||||
Comments:
|
||||
|
||||
* Low variance -- training loss is consistently a bit lower than validation
|
||||
loss.
|
||||
* Since images are "artificially" labeled, it is hard to say what the bias is.
|
||||
There should be some bias since this network does not overfit, and it also
|
||||
looks like training converges after 100 epochs (with decaying learning rate).
|
||||
* There may also be labeling noise. It is indeed suspicious that the validation
|
||||
loss converges with very low variance. Perhaps revisit the labeling
|
||||
approach for the base generator.
|
||||
* Conclusion: Check labeling, then bring out the big guns and expand the
|
||||
residual net.
|
||||
|
||||
With this model as a basis for the 9 regions, followed by a reshape, conv and
|
||||
two dense layers, yields ok performance: Around 0.20 loss after few epochs.
|
||||
|
||||
However, validation loss is often lower than training loss. It might be that
|
||||
the two distributions are not the same for both networks -- check the random
|
||||
seed and verify! Might also be noisy training (because of augmentation).
|
||||
|
||||
12
icenet/__init__.py
Normal file
12
icenet/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
try:
|
||||
from icenet import base_cnn, icenet
|
||||
except Exception as ex:
|
||||
print("Failed to import keras")
|
||||
print(ex)
|
||||
base_cnn = icenet = None
|
||||
|
||||
MODELS = {
|
||||
'base_cnn': base_cnn,
|
||||
'icenet': icenet
|
||||
}
|
||||
280
icenet/base_cnn.py
Normal file
280
icenet/base_cnn.py
Normal file
@ -0,0 +1,280 @@
|
||||
"""Base CNN."""
|
||||
|
||||
import random
|
||||
import numpy as np
|
||||
from itertools import islice
|
||||
from icenet import util
|
||||
from icenet import residual
|
||||
from icenet import resnet
|
||||
|
||||
from keras.models import Model
|
||||
from keras.initializers import glorot_uniform
|
||||
from keras.optimizers import Adam, SGD
|
||||
from keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
|
||||
from keras.models import load_model
|
||||
from keras.layers import (
|
||||
Input, Add, Dense, Activation, Conv2D, BatchNormalization, Flatten,
|
||||
MaxPooling2D, AveragePooling2D, Dropout, Concatenate, ZeroPadding2D
|
||||
)
|
||||
|
||||
|
||||
MODEL_NAME = 'base_cnn'
|
||||
|
||||
|
||||
def get_model_simple(img_shape):
|
||||
X_img = Input(img_shape)
|
||||
X = ZeroPadding2D((2, 2))(X_img)
|
||||
|
||||
# Conv 1
|
||||
X = Conv2D(64,
|
||||
kernel_size=(5, 5),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv1')(X)
|
||||
X = Conv2D(128,
|
||||
kernel_size=(3, 3),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv2')(X)
|
||||
X = MaxPooling2D((3, 3), strides=(2, 2))(X)
|
||||
X = Dropout(0.2)(X)
|
||||
|
||||
X = Conv2D(128,
|
||||
kernel_size=(3, 3),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv3')(X)
|
||||
X = MaxPooling2D((2, 2), strides=(2, 2))(X)
|
||||
X = Dropout(0.2)(X)
|
||||
|
||||
X = Conv2D(64,
|
||||
kernel_size=(3, 3),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv4')(X)
|
||||
X = MaxPooling2D((3, 3), strides=(1, 1))(X)
|
||||
X = Dropout(0.2)(X)
|
||||
|
||||
X = Flatten()(X)
|
||||
X = Dense(512,
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='fc1')(X)
|
||||
X = Dropout(0.2)(X)
|
||||
|
||||
X = Dense(256,
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='fc2')(X)
|
||||
X = Dropout(0.2)(X)
|
||||
|
||||
X = Dense(3,
|
||||
activation='sigmoid',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='y_hat')(X)
|
||||
|
||||
return Model(inputs=X_img, outputs=X, name=MODEL_NAME)
|
||||
|
||||
|
||||
def get_model_residual(img_shape):
|
||||
X_img = Input(img_shape)
|
||||
|
||||
X = ZeroPadding2D((2, 2))(X_img)
|
||||
|
||||
# Conv 1
|
||||
X = Conv2D(32,
|
||||
kernel_size=(5, 5),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv1')(X)
|
||||
X = BatchNormalization(axis=3, name='bn_conv1')(X)
|
||||
X = Activation('relu')(X)
|
||||
X = MaxPooling2D((2, 2), strides=(2, 2))(X)
|
||||
|
||||
# Conv 2 (residual)
|
||||
X = residual.convolutional_block(X, 4, (32, 32, 128), 'stage2a', s=1)
|
||||
X = residual.identity_block(X, 4, (32, 32, 128), 'stage2b')
|
||||
#X = residual.identity_block(X, 4, (32, 32, 128), 'stage2c')
|
||||
|
||||
# Conv 3 (residual)
|
||||
X = residual.convolutional_block(X, 3, (64, 64, 256), 'stage3a', s=2)
|
||||
X = residual.identity_block(X, 3, (64, 64, 256), 'stage3b')
|
||||
X = residual.identity_block(X, 3, (64, 64, 256), 'stage3c')
|
||||
X = residual.identity_block(X, 3, (64, 64, 256), 'stage3d')
|
||||
|
||||
# Conv 4 (residual)
|
||||
X = residual.convolutional_block(X, 3, (128, 128, 512), 'stage4a', s=2)
|
||||
X = residual.identity_block(X, 3, (128, 128, 512), 'stage4b')
|
||||
X = residual.identity_block(X, 3, (128, 128, 512), 'stage4c')
|
||||
#X = residual.identity_block(X, 3, (128, 128, 512), 'stage4d')
|
||||
#X = residual.identity_block(X, 3, (128, 128, 512), 'stage4e')
|
||||
|
||||
#X = AveragePooling2D(pool_size=(4, 4), name='avg_pool')(X)
|
||||
X = Conv2D(512,
|
||||
kernel_size=(4, 4),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='convend')(X)
|
||||
X = BatchNormalization(axis=3, name='bn_convend')(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Flatten
|
||||
X = Flatten()(X)
|
||||
|
||||
X = Dense(3,
|
||||
activation='softmax',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='y_hat')(X)
|
||||
|
||||
return Model(inputs=X_img, outputs=X, name=MODEL_NAME)
|
||||
|
||||
|
||||
def get_model_res18(img_shape):
|
||||
X_img = Input(img_shape)
|
||||
|
||||
X = ZeroPadding2D((2, 2))(X_img)
|
||||
|
||||
# Conv 1
|
||||
X = Conv2D(64,
|
||||
kernel_size=(5, 5),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv1')(X)
|
||||
X = BatchNormalization(axis=3, name='bn_conv1')(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Conv 2 (residual)
|
||||
X = residual.basic_block(X, 3, (64, 64), 'stage2a')
|
||||
X = residual.basic_block(X, 3, (64, 64), 'stage2b')
|
||||
|
||||
# Conv 3 (residual)
|
||||
X = residual.basic_block(X, 3, (128, 128), 'stage3a')
|
||||
X = residual.basic_block(X, 3, (128, 128), 'stage3b')
|
||||
|
||||
# Conv 4 (residual)
|
||||
X = residual.basic_block(X, 3, (256, 256), 'stage4a')
|
||||
X = residual.basic_block(X, 3, (256, 256), 'stage4b')
|
||||
|
||||
# Conv 5 (residual)
|
||||
X = residual.basic_block(X, 3, (512, 512), 'stage5a')
|
||||
X = residual.basic_block(X, 3, (512, 512), 'stage5b')
|
||||
|
||||
# AveragePool
|
||||
X = AveragePooling2D(pool_size=(2, 2), name='avg_pool')(X)
|
||||
|
||||
# Flatten
|
||||
X = Flatten()(X)
|
||||
|
||||
X = Dense(3,
|
||||
activation='softmax',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='y_hat')(X)
|
||||
|
||||
return Model(inputs=X_img, outputs=X, name=MODEL_NAME)
|
||||
|
||||
|
||||
def train(datadir):
|
||||
print("Load samples from train.json ...")
|
||||
samples = util.load_samples(datadir, 'train.json')
|
||||
m_tot = len(samples)
|
||||
print("Got %d samples" % m_tot)
|
||||
|
||||
# Split the samples with the same seed every time so that the dev set
|
||||
# is "frozen". This makes it easier to monitor and compare different models.
|
||||
# When a good model is found, the dev set can then be removed entirely
|
||||
# in order to fully utilize the available training data.
|
||||
split = 0.90
|
||||
train_samples, dev_samples = util.train_dev_split(samples, split, shuffle=True)
|
||||
m_train = len(train_samples)
|
||||
m_dev = len(dev_samples)
|
||||
print("Split train/test = %.2f" % split)
|
||||
print("Training samples: %d" % m_train)
|
||||
print("Dev samples: %d" % m_dev)
|
||||
print("First 5 dev samples ID's:")
|
||||
print(' '.join([s['id'] for s in dev_samples[0:5]]))
|
||||
|
||||
# The minimum/maximum values of the entire training set will determine
|
||||
# the scaling factors. We store the scaling factors to file so that they
|
||||
# can be retrieved and re-used for predictions.
|
||||
minmax = util.get_minmax(train_samples)
|
||||
print("Write scaling to %s-scaling.csv" % MODEL_NAME)
|
||||
util.save_minmax(datadir, '%s-scaling.pkl' % MODEL_NAME, minmax)
|
||||
|
||||
# Since we make heavy use of augmentation here, we can also augment
|
||||
# the dev set just to smooth out validation losses during training.
|
||||
# Augment the dev set x10.
|
||||
m_dev *= 10
|
||||
print("Dev samples (augmented): %d" % m_dev)
|
||||
dev_generator = util.base_cnn_generator(
|
||||
dev_samples, minmax, m_dev
|
||||
)
|
||||
X_dev, Y_dev = list(islice(dev_generator, 1))[0]
|
||||
|
||||
# Model + optimization parameters.
|
||||
#model = get_model_res18((28, 28, 2))
|
||||
model = resnet.ResnetBuilder.build_resnet_18((2, 28, 28), 3)
|
||||
batch_size = 32
|
||||
#opt = Adam(lr=0.0002, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
|
||||
opt = SGD(lr=0.005, momentum=0.9, decay=1e-5)
|
||||
model.compile(
|
||||
optimizer=opt,
|
||||
loss="categorical_crossentropy",
|
||||
metrics = ["accuracy"]
|
||||
)
|
||||
model.summary()
|
||||
|
||||
# Callbacks
|
||||
callbacks = [
|
||||
ModelCheckpoint(
|
||||
filepath=util.model_fp(datadir, '%s-weights-val_loss.h5' % MODEL_NAME),
|
||||
verbose=1,
|
||||
monitor='val_loss',
|
||||
save_best_only=True
|
||||
),
|
||||
ModelCheckpoint(
|
||||
filepath=util.model_fp(datadir, '%s-weights-loss.h5' % MODEL_NAME),
|
||||
verbose=1,
|
||||
monitor='loss',
|
||||
save_best_only=True
|
||||
),
|
||||
ReduceLROnPlateau(
|
||||
monitor='val_loss',
|
||||
factor=0.1,
|
||||
verbose=1,
|
||||
patience=8,
|
||||
epsilon=0.005,
|
||||
mode='min',
|
||||
min_lr=1e-7
|
||||
),
|
||||
EarlyStopping(
|
||||
'loss',
|
||||
patience=30,
|
||||
mode="min"
|
||||
)
|
||||
]
|
||||
|
||||
# TRAIN!
|
||||
model.fit_generator(
|
||||
util.base_cnn_generator(train_samples, minmax, batch_size),
|
||||
steps_per_epoch=int(4*m_train/batch_size),
|
||||
epochs=1000,
|
||||
validation_data=(X_dev, Y_dev),
|
||||
callbacks=callbacks
|
||||
)
|
||||
|
||||
score = model.evaluate(X_dev, Y_dev)
|
||||
print("")
|
||||
print("Test loss: %.4f" % score[0])
|
||||
print("Test accuracy: %.4f" % score[1])
|
||||
|
||||
332
icenet/icenet.py
Normal file
332
icenet/icenet.py
Normal file
@ -0,0 +1,332 @@
|
||||
"""Standard CNN with data augmentation."""
|
||||
|
||||
import random
|
||||
import numpy as np
|
||||
from itertools import islice
|
||||
from icenet import util
|
||||
|
||||
from keras.models import Model
|
||||
from keras.initializers import glorot_uniform
|
||||
from keras.optimizers import Adam
|
||||
from keras import backend as K
|
||||
from keras.callbacks import ModelCheckpoint, EarlyStopping, LearningRateScheduler
|
||||
from keras.models import load_model
|
||||
from keras.layers import (
|
||||
Input, Dense, Activation, Conv2D, BatchNormalization, Flatten,
|
||||
MaxPooling2D, Dropout, Concatenate, Reshape
|
||||
)
|
||||
|
||||
|
||||
MODEL_NAME = 'icenet'
|
||||
|
||||
|
||||
def get_base(base_model_filepath, postfix, last_layer, freeze=True):
|
||||
base_model = load_model(base_model_filepath)
|
||||
for i,l in enumerate(base_model.layers[0:last_layer+1]):
|
||||
l.trainable = not (i == 0 or freeze)
|
||||
l.name = '%s_%s' % (l.name, postfix)
|
||||
#base_model.layers[last_layer].trainable = True # Should be avrg pool layer
|
||||
return base_model.input, base_model.layers[last_layer].output
|
||||
|
||||
|
||||
def get_model(base_model_filepath):
|
||||
|
||||
print("Load base models ...")
|
||||
X_inputs, X_base_outputs = list(zip(*[
|
||||
get_base(base_model_filepath, 'sec%d' % i, -5, freeze=True) for i in range(9)
|
||||
]))
|
||||
print("Output layer:")
|
||||
print(X_base_outputs[0])
|
||||
|
||||
#X = Concatenate(axis=2)(list(X_base_outputs))
|
||||
#X = Reshape((nw*3, nh*3, nc))(X)
|
||||
#_, n = K.int_shape(X_base_outputs[0])
|
||||
_, nw, nh, nc = K.int_shape(X_base_outputs[0])
|
||||
assert nw == 1 and nh == 1 and nc == 512
|
||||
X = Concatenate(axis=-1)(list(X_base_outputs))
|
||||
X = Reshape((3, 3, nc))(X)
|
||||
|
||||
X = Conv2D(1024,
|
||||
kernel_size=(3, 3),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='conv_top')(X)
|
||||
#X = MaxPooling2D((3, 3))(X)
|
||||
|
||||
X = Flatten()(X)
|
||||
X = Dropout(0.25)(X)
|
||||
X = Dense(1024,
|
||||
activation='relu',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='fc1')(X)
|
||||
X = Dropout(0.25)(X)
|
||||
|
||||
X = Dense(1,
|
||||
activation='sigmoid',
|
||||
kernel_initializer=glorot_uniform(seed=0),
|
||||
name='y_hat')(X)
|
||||
|
||||
return Model(inputs=list(X_inputs), outputs=X, name=MODEL_NAME)
|
||||
|
||||
|
||||
def train(datadir):
|
||||
base_model_name = 'base_cnn'
|
||||
print("Load scaling from %s-scaling.csv" % base_model_name)
|
||||
minmax = util.load_minmax(datadir, '%s-scaling.pkl' % base_model_name)
|
||||
|
||||
print("Load samples from train.json ...")
|
||||
samples = util.load_samples(datadir, 'train.json')
|
||||
m_tot = len(samples)
|
||||
print("Got %d samples" % m_tot)
|
||||
|
||||
split = 0.90
|
||||
train_samples, dev_samples = util.train_dev_split(samples, split)
|
||||
m_train = len(train_samples)
|
||||
m_dev = len(dev_samples)
|
||||
print("Split train/test = %.2f" % split)
|
||||
print("Training samples: %d" % m_train)
|
||||
print("Dev samples: %d" % m_dev)
|
||||
print("First 5 dev samples ID's:")
|
||||
print(' '.join([s['id'] for s in dev_samples[0:5]]))
|
||||
|
||||
# Extract dev_samples
|
||||
dev_generator = util.icenet_generator(
|
||||
dev_samples, minmax, m_dev, crop_offset=3,
|
||||
augment=False
|
||||
)
|
||||
X_dev, Y_dev = list(islice(dev_generator, 1))[0]
|
||||
|
||||
# Model + opt
|
||||
def lr_schedule(epoch):
|
||||
if epoch < 20:
|
||||
return 0.0005
|
||||
elif epoch < 50:
|
||||
return 0.0002
|
||||
elif epoch < 200:
|
||||
return 0.00005
|
||||
else:
|
||||
return 0.00001
|
||||
model = get_model(util.model_fp(datadir, '%s-weights-val_loss.h5' % base_model_name))
|
||||
batch_size = 16
|
||||
opt = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
|
||||
model.compile(
|
||||
optimizer=opt,
|
||||
loss="binary_crossentropy",
|
||||
metrics = ["accuracy"]
|
||||
)
|
||||
_summary = []
|
||||
model.summary(print_fn=lambda x: _summary.append(x))
|
||||
print('\n'.join(_summary[0:8]))
|
||||
print("...")
|
||||
print("...")
|
||||
print("...")
|
||||
print("...")
|
||||
print('\n'.join(_summary[-40:]))
|
||||
|
||||
# Callbacks
|
||||
callbacks = [
|
||||
ModelCheckpoint(
|
||||
filepath=util.model_fp(datadir, '%s-weights-val_loss.h5' % MODEL_NAME),
|
||||
verbose=1,
|
||||
monitor='val_loss',
|
||||
save_best_only=True
|
||||
),
|
||||
ModelCheckpoint(
|
||||
filepath=util.model_fp(datadir, '%s-weights-loss.h5' % MODEL_NAME),
|
||||
verbose=1,
|
||||
monitor='loss',
|
||||
save_best_only=True
|
||||
),
|
||||
LearningRateScheduler(lr_schedule),
|
||||
EarlyStopping(
|
||||
'loss',
|
||||
patience=50,
|
||||
mode="min"
|
||||
)
|
||||
]
|
||||
|
||||
# TRAIN!
|
||||
model.fit_generator(
|
||||
util.icenet_generator(train_samples, minmax, batch_size, crop_offset=3),
|
||||
steps_per_epoch=int(2*m_train/batch_size),
|
||||
epochs=1000,
|
||||
validation_data=(X_dev, Y_dev),
|
||||
callbacks=callbacks
|
||||
)
|
||||
|
||||
score = model.evaluate(X_dev, Y_dev)
|
||||
print("")
|
||||
print("Test loss: %.4f" % score[0])
|
||||
print("Test accuracy: %.4f" % score[1])
|
||||
|
||||
print("Make dev predictions ...")
|
||||
y_pred = model.predict(X_dev, batch_size=32)
|
||||
print("Write to %s-dev.csv" % MODEL_NAME)
|
||||
util.write_preds(datadir, '%s-dev.csv' % MODEL_NAME, dev_samples, y_pred)
|
||||
|
||||
n_test = 30
|
||||
print("Check invariance on %d random augmented dev samples" % n_test)
|
||||
for i in range(n_test):
|
||||
test_sample = random.choice(dev_samples)
|
||||
dev_generator = util.icenet_generator(
|
||||
[test_sample], minmax, 6*6*4*2, crop_offset=3,
|
||||
augment=True
|
||||
)
|
||||
X_dev, Y_dev = list(islice(dev_generator, 1))[0]
|
||||
y_pred = model.predict(X_dev, batch_size=32)
|
||||
print("Dev sample %s (is_iceberg=%s): Mean = %.4f, std = %.4f, min = %.4f, max = %.4f" % (
|
||||
test_sample.get('id'), test_sample.get('is_iceberg'),
|
||||
np.mean(y_pred), np.std(y_pred),
|
||||
np.min(y_pred), np.max(y_pred)
|
||||
))
|
||||
|
||||
|
||||
|
||||
def predict(datadir):
|
||||
print("Load model from %s-weights-loss.h5 ..." % MODEL_NAME)
|
||||
model = load_model(util.model_fp(datadir, '%s-weights-loss.h5' % MODEL_NAME))
|
||||
|
||||
# Load scaling factors
|
||||
base_model_name = 'base_cnn'
|
||||
print("Load scaling from %s-scaling.pkl" % base_model_name)
|
||||
minmax = util.load_minmax(datadir, '%s-scaling.pkl' % base_model_name)
|
||||
|
||||
target_fp = 'train.json'
|
||||
print("Load samples from %s..." % target_fp)
|
||||
samples = util.load_samples(datadir, target_fp)
|
||||
m_tot = len(samples)
|
||||
print("Got %d samples" % m_tot)
|
||||
|
||||
# Extract samples with generator
|
||||
data_gen = util.icenet_generator(
|
||||
samples, minmax, m_tot, crop_offset=3,
|
||||
augment=False
|
||||
)
|
||||
X, _= list(islice(data_gen, 1))[0]
|
||||
print("X (image) shape:")
|
||||
print(X[0].shape)
|
||||
|
||||
# Predict ...
|
||||
print("Predict!")
|
||||
y_pred = model.predict(X, batch_size=32)
|
||||
|
||||
filename = '%s-%s.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred)
|
||||
|
||||
n_test = 20
|
||||
print("Check invariance on %d random augmented dev samples" % n_test)
|
||||
for i in range(n_test):
|
||||
test_sample = random.choice(samples)
|
||||
dev_generator = util.icenet_generator(
|
||||
[test_sample], minmax, 6*6*4*2, crop_offset=3,
|
||||
augment=True
|
||||
)
|
||||
X_dev, Y_dev = list(islice(dev_generator, 1))[0]
|
||||
y_pred = model.predict(X_dev, batch_size=32)
|
||||
print("Dev sample %s (is_iceberg=%s): Mean = %.4f, std = %.4f, min = %.4f, max = %.4f" % (
|
||||
test_sample.get('id'), test_sample.get('is_iceberg'),
|
||||
np.mean(y_pred), np.std(y_pred),
|
||||
np.min(y_pred), np.max(y_pred)
|
||||
))
|
||||
|
||||
|
||||
def _sigmoid(z):
|
||||
return 1.0 / (1 + np.exp(-z))
|
||||
|
||||
|
||||
def stretch(z, factor=10):
|
||||
return _sigmoid(factor*(z-0.5))
|
||||
|
||||
|
||||
def predict_avrg(datadir):
|
||||
print("Load model from %s-weights-loss.h5 ..." % MODEL_NAME)
|
||||
model = load_model(util.model_fp(datadir, '%s-weights-loss.h5' % MODEL_NAME))
|
||||
|
||||
# Load scaling factors
|
||||
base_model_name = 'base_cnn'
|
||||
print("Load scaling from %s-scaling.pkl" % base_model_name)
|
||||
minmax = util.load_minmax(datadir, '%s-scaling.pkl' % base_model_name)
|
||||
|
||||
target_fp = 'train.json'
|
||||
print("Load samples from %s..." % target_fp)
|
||||
samples = util.load_samples(datadir, target_fp)
|
||||
random.shuffle(samples)
|
||||
samples = samples[0:400]
|
||||
random.shuffle(samples)
|
||||
m_tot = len(samples)
|
||||
print("Got %d samples" % m_tot)
|
||||
|
||||
n_test = 6*6*4*2
|
||||
y_pred_first = np.zeros((len(samples), 1))
|
||||
y_pred_avrg = np.zeros((len(samples), 1))
|
||||
y_pred_avrg_sig10 = np.zeros((len(samples), 1))
|
||||
y_pred_avrg_sig20 = np.zeros((len(samples), 1))
|
||||
y_pred_avrg_sig30 = np.zeros((len(samples), 1))
|
||||
y_pred_avrg_sig40 = np.zeros((len(samples), 1))
|
||||
y_pred_avrg_sig50 = np.zeros((len(samples), 1))
|
||||
y_reals = np.zeros((len(samples), 1))
|
||||
print("Average each sample over %d augmented versions" % n_test)
|
||||
for i,s in enumerate(samples):
|
||||
dev_generator = util.icenet_generator(
|
||||
[s], minmax, n_test, crop_offset=3,
|
||||
augment=True
|
||||
)
|
||||
X_dev, Y_dev = list(islice(dev_generator, 1))[0]
|
||||
y_pred = model.predict(X_dev, batch_size=n_test)
|
||||
|
||||
y_pred_first[i,0] = y_pred[0,0]
|
||||
y_pred_avrg[i,0] = np.mean(y_pred)
|
||||
y_pred_avrg_sig10[i,0] = stretch(y_pred_avrg[i,0], factor=10)
|
||||
y_pred_avrg_sig20[i,0] = stretch(y_pred_avrg[i,0], factor=11)
|
||||
y_pred_avrg_sig30[i,0] = stretch(y_pred_avrg[i,0], factor=12)
|
||||
y_pred_avrg_sig40[i,0] = stretch(y_pred_avrg[i,0], factor=13)
|
||||
y_pred_avrg_sig50[i,0] = stretch(y_pred_avrg[i,0], factor=14)
|
||||
y_reals[i,0] = s.get('is_iceberg', 0) * 1.0
|
||||
|
||||
print("Sample %d: %s (iceberg=%s): Mean = %.4f, s(10) = %.4f, s(40) = %.4f std = %.4f, min = %.4f, max = %.4f" % (
|
||||
i, s.get('id'), s.get('is_iceberg'),
|
||||
np.mean(y_pred),
|
||||
y_pred_avrg_sig10[i,0], y_pred_avrg_sig40[i,0],
|
||||
np.std(y_pred),
|
||||
np.min(y_pred), np.max(y_pred)
|
||||
))
|
||||
|
||||
print("'First' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_first))
|
||||
print("'Avrg' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg))
|
||||
print("'Avrg stretch(10)' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg_sig10))
|
||||
print("'Avrg stretch(20)' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg_sig20))
|
||||
print("'Avrg stretch(30)' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg_sig30))
|
||||
print("'Avrg stretch(40)' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg_sig40))
|
||||
print("'Avrg stretch(50)' loss: %.4f" % util.binary_crossentropy(y_reals, y_pred_avrg_sig50))
|
||||
|
||||
filename = '%s-%s-avrg.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg)
|
||||
|
||||
filename = '%s-%s-fst.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_first)
|
||||
|
||||
filename = '%s-%s-s10.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg_sig10)
|
||||
|
||||
filename = '%s-%s-s20.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg_sig20)
|
||||
|
||||
filename = '%s-%s-s30.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg_sig30)
|
||||
|
||||
filename = '%s-%s-s40.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg_sig40)
|
||||
|
||||
filename = '%s-%s-s50.csv' % (MODEL_NAME, target_fp.split('.')[0])
|
||||
print("Write to %s" % filename)
|
||||
util.write_preds(datadir, filename, samples, y_pred_avrg_sig50)
|
||||
|
||||
190
icenet/residual.py
Normal file
190
icenet/residual.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""Residual blocks: Conv and identity"""
|
||||
|
||||
from keras.initializers import glorot_uniform
|
||||
from keras.layers import Add, Activation, Conv2D, BatchNormalization
|
||||
|
||||
|
||||
def basic_block(X, f, filters, s, label):
|
||||
"""
|
||||
Residual net identity block.
|
||||
|
||||
Arguments:
|
||||
X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev)
|
||||
f -- integer, specifying the shape of the middle CONV's window for the main path
|
||||
filters -- python list of integers, defining the number of filters in the CONV layers of the main path
|
||||
label -- label to use for naming
|
||||
s -- strides
|
||||
|
||||
Returns:
|
||||
X -- output of the identity block, tensor of shape (n_H, n_W, n_C)
|
||||
"""
|
||||
F1, F2 = filters
|
||||
X_shortcut = X
|
||||
|
||||
# First component of main path
|
||||
X = Conv2D(
|
||||
F1,
|
||||
kernel_size=(f, f),
|
||||
strides=(s, s),
|
||||
padding='same',
|
||||
name='conv_%s_2a' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2a' % label)(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Second component of main path
|
||||
X = Conv2D(
|
||||
F2,
|
||||
kernel_size=(f, f),
|
||||
strides=(1, 1),
|
||||
padding='same',
|
||||
name='conv_%s_2b' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2b' % label)(X)
|
||||
|
||||
# Shortcut path
|
||||
X_shortcut = Conv2D(
|
||||
F2,
|
||||
kernel_size=(1, 1),
|
||||
strides=(s, s),
|
||||
padding='valid',
|
||||
name='conv_%s_1' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X_shortcut)
|
||||
X_shortcut = BatchNormalization(axis=3, name='bn_%s_1' % label)(X_shortcut)
|
||||
|
||||
# Add shortcut value to main path, and pass it through a RELU activation
|
||||
X = Add()([X_shortcut, X])
|
||||
X = Activation('relu')(X)
|
||||
|
||||
return X
|
||||
|
||||
|
||||
def identity_block(X, f, filters, label):
|
||||
"""
|
||||
Residual net identity block.
|
||||
|
||||
Arguments:
|
||||
X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev)
|
||||
f -- integer, specifying the shape of the middle CONV's window for the main path
|
||||
filters -- python list of integers, defining the number of filters in the CONV layers of the main path
|
||||
label -- label to use for naming
|
||||
|
||||
Returns:
|
||||
X -- output of the identity block, tensor of shape (n_H, n_W, n_C)
|
||||
"""
|
||||
F1, F2, F3 = filters
|
||||
X_shortcut = X
|
||||
|
||||
# First component of main path
|
||||
X = Conv2D(
|
||||
F1,
|
||||
kernel_size=(1, 1),
|
||||
strides=(1,1),
|
||||
padding='valid',
|
||||
name='conv_%s_2a' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2a' % label)(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Second component of main path
|
||||
X = Conv2D(
|
||||
F2,
|
||||
kernel_size=(f, f),
|
||||
strides=(1, 1),
|
||||
padding='same',
|
||||
name='conv_%s_2b' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2b' % label)(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Third component of main path
|
||||
X = Conv2D(
|
||||
F3,
|
||||
kernel_size=(1, 1),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
name='conv_%s_2c' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2c' % label)(X)
|
||||
|
||||
# Add shortcut value to main path, and pass it through a RELU activation
|
||||
X = Add()([X_shortcut, X])
|
||||
X = Activation('relu')(X)
|
||||
|
||||
return X
|
||||
|
||||
|
||||
def convolutional_block(X, f, filters, label, s=2):
|
||||
"""
|
||||
Residual net convolutional block.
|
||||
|
||||
Arguments:
|
||||
X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev)
|
||||
f -- integer, specifying the shape of the middle CONV's window for the main path
|
||||
filters -- python list of integers, defining the number of filters in the CONV layers of the main path
|
||||
label -- label to use for naming
|
||||
s -- Integer, specifying the stride to be used
|
||||
|
||||
Returns:
|
||||
X -- output of the convolutional block, tensor of shape (n_H, n_W, n_C)
|
||||
"""
|
||||
F1, F2, F3 = filters
|
||||
X_shortcut = X
|
||||
|
||||
# First component of main path
|
||||
X = Conv2D(
|
||||
F1,
|
||||
kernel_size=(1, 1),
|
||||
strides=(s,s),
|
||||
padding='valid',
|
||||
name='conv_%s_2a' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2a' % label)(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Second component of main path
|
||||
X = Conv2D(
|
||||
F2,
|
||||
kernel_size=(f, f),
|
||||
strides=(1, 1),
|
||||
padding='same',
|
||||
name='conv_%s_2b' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2b' % label)(X)
|
||||
X = Activation('relu')(X)
|
||||
|
||||
# Third component of main path
|
||||
X = Conv2D(
|
||||
F3,
|
||||
kernel_size=(1, 1),
|
||||
strides=(1, 1),
|
||||
padding='valid',
|
||||
name='conv_%s_2c' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X)
|
||||
X = BatchNormalization(axis=3, name='bn_%s_2c' % label)(X)
|
||||
|
||||
# Shortcut path
|
||||
X_shortcut = Conv2D(
|
||||
F3,
|
||||
kernel_size=(1, 1),
|
||||
strides=(s, s),
|
||||
padding='valid',
|
||||
name='conv_%s_1' % label,
|
||||
kernel_initializer=glorot_uniform(seed=0)
|
||||
)(X_shortcut)
|
||||
X_shortcut = BatchNormalization(axis=3, name='bn_%s_1' % label)(X_shortcut)
|
||||
|
||||
# Add shortcut value to main path, and pass it through a RELU activation
|
||||
X = Add()([X_shortcut, X])
|
||||
X = Activation('relu')(X)
|
||||
|
||||
return X
|
||||
252
icenet/resnet.py
Normal file
252
icenet/resnet.py
Normal file
@ -0,0 +1,252 @@
|
||||
from __future__ import division
|
||||
|
||||
import six
|
||||
from keras.models import Model
|
||||
from keras.layers import (
|
||||
Input,
|
||||
Activation,
|
||||
Dense,
|
||||
Flatten
|
||||
)
|
||||
from keras.layers.convolutional import (
|
||||
Conv2D,
|
||||
MaxPooling2D,
|
||||
AveragePooling2D
|
||||
)
|
||||
from keras.layers.merge import add
|
||||
from keras.layers.normalization import BatchNormalization
|
||||
from keras.regularizers import l2
|
||||
from keras import backend as K
|
||||
|
||||
|
||||
def _bn_relu(input):
|
||||
"""Helper to build a BN -> relu block
|
||||
"""
|
||||
norm = BatchNormalization(axis=CHANNEL_AXIS)(input)
|
||||
return Activation("relu")(norm)
|
||||
|
||||
|
||||
def _conv_bn_relu(**conv_params):
|
||||
"""Helper to build a conv -> BN -> relu block
|
||||
"""
|
||||
filters = conv_params["filters"]
|
||||
kernel_size = conv_params["kernel_size"]
|
||||
strides = conv_params.setdefault("strides", (1, 1))
|
||||
kernel_initializer = conv_params.setdefault("kernel_initializer", "he_normal")
|
||||
padding = conv_params.setdefault("padding", "same")
|
||||
kernel_regularizer = conv_params.setdefault("kernel_regularizer", l2(1.e-4))
|
||||
|
||||
def f(input):
|
||||
conv = Conv2D(filters=filters, kernel_size=kernel_size,
|
||||
strides=strides, padding=padding,
|
||||
kernel_initializer=kernel_initializer,
|
||||
kernel_regularizer=kernel_regularizer)(input)
|
||||
return _bn_relu(conv)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def _bn_relu_conv(**conv_params):
|
||||
"""Helper to build a BN -> relu -> conv block.
|
||||
This is an improved scheme proposed in http://arxiv.org/pdf/1603.05027v2.pdf
|
||||
"""
|
||||
filters = conv_params["filters"]
|
||||
kernel_size = conv_params["kernel_size"]
|
||||
strides = conv_params.setdefault("strides", (1, 1))
|
||||
kernel_initializer = conv_params.setdefault("kernel_initializer", "he_normal")
|
||||
padding = conv_params.setdefault("padding", "same")
|
||||
kernel_regularizer = conv_params.setdefault("kernel_regularizer", l2(1.e-4))
|
||||
|
||||
def f(input):
|
||||
activation = _bn_relu(input)
|
||||
return Conv2D(filters=filters, kernel_size=kernel_size,
|
||||
strides=strides, padding=padding,
|
||||
kernel_initializer=kernel_initializer,
|
||||
kernel_regularizer=kernel_regularizer)(activation)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def _shortcut(input, residual):
|
||||
"""Adds a shortcut between input and residual block and merges them with "sum"
|
||||
"""
|
||||
# Expand channels of shortcut to match residual.
|
||||
# Stride appropriately to match residual (width, height)
|
||||
# Should be int if network architecture is correctly configured.
|
||||
input_shape = K.int_shape(input)
|
||||
residual_shape = K.int_shape(residual)
|
||||
stride_width = int(round(input_shape[ROW_AXIS] / residual_shape[ROW_AXIS]))
|
||||
stride_height = int(round(input_shape[COL_AXIS] / residual_shape[COL_AXIS]))
|
||||
equal_channels = input_shape[CHANNEL_AXIS] == residual_shape[CHANNEL_AXIS]
|
||||
|
||||
shortcut = input
|
||||
# 1 X 1 conv if shape is different. Else identity.
|
||||
if stride_width > 1 or stride_height > 1 or not equal_channels:
|
||||
shortcut = Conv2D(filters=residual_shape[CHANNEL_AXIS],
|
||||
kernel_size=(1, 1),
|
||||
strides=(stride_width, stride_height),
|
||||
padding="valid",
|
||||
kernel_initializer="he_normal",
|
||||
kernel_regularizer=l2(0.0001))(input)
|
||||
|
||||
return add([shortcut, residual])
|
||||
|
||||
|
||||
def _residual_block(block_function, filters, repetitions, is_first_layer=False):
|
||||
"""Builds a residual block with repeating bottleneck blocks.
|
||||
"""
|
||||
def f(input):
|
||||
for i in range(repetitions):
|
||||
init_strides = (1, 1)
|
||||
if i == 0 and not is_first_layer:
|
||||
init_strides = (2, 2)
|
||||
input = block_function(filters=filters, init_strides=init_strides,
|
||||
is_first_block_of_first_layer=(is_first_layer and i == 0))(input)
|
||||
return input
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def basic_block(filters, init_strides=(1, 1), is_first_block_of_first_layer=False):
|
||||
"""Basic 3 X 3 convolution blocks for use on resnets with layers <= 34.
|
||||
Follows improved proposed scheme in http://arxiv.org/pdf/1603.05027v2.pdf
|
||||
"""
|
||||
def f(input):
|
||||
|
||||
if is_first_block_of_first_layer:
|
||||
# don't repeat bn->relu since we just did bn->relu->maxpool
|
||||
conv1 = Conv2D(filters=filters, kernel_size=(3, 3),
|
||||
strides=init_strides,
|
||||
padding="same",
|
||||
kernel_initializer="he_normal",
|
||||
kernel_regularizer=l2(1e-4))(input)
|
||||
else:
|
||||
conv1 = _bn_relu_conv(filters=filters, kernel_size=(3, 3),
|
||||
strides=init_strides)(input)
|
||||
|
||||
residual = _bn_relu_conv(filters=filters, kernel_size=(3, 3))(conv1)
|
||||
return _shortcut(input, residual)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def bottleneck(filters, init_strides=(1, 1), is_first_block_of_first_layer=False):
|
||||
"""Bottleneck architecture for > 34 layer resnet.
|
||||
Follows improved proposed scheme in http://arxiv.org/pdf/1603.05027v2.pdf
|
||||
|
||||
Returns:
|
||||
A final conv layer of filters * 4
|
||||
"""
|
||||
def f(input):
|
||||
|
||||
if is_first_block_of_first_layer:
|
||||
# don't repeat bn->relu since we just did bn->relu->maxpool
|
||||
conv_1_1 = Conv2D(filters=filters, kernel_size=(1, 1),
|
||||
strides=init_strides,
|
||||
padding="same",
|
||||
kernel_initializer="he_normal",
|
||||
kernel_regularizer=l2(1e-4))(input)
|
||||
else:
|
||||
conv_1_1 = _bn_relu_conv(filters=filters, kernel_size=(1, 1),
|
||||
strides=init_strides)(input)
|
||||
|
||||
conv_3_3 = _bn_relu_conv(filters=filters, kernel_size=(3, 3))(conv_1_1)
|
||||
residual = _bn_relu_conv(filters=filters * 4, kernel_size=(1, 1))(conv_3_3)
|
||||
return _shortcut(input, residual)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def _handle_dim_ordering():
|
||||
global ROW_AXIS
|
||||
global COL_AXIS
|
||||
global CHANNEL_AXIS
|
||||
if K.image_dim_ordering() == 'tf':
|
||||
ROW_AXIS = 1
|
||||
COL_AXIS = 2
|
||||
CHANNEL_AXIS = 3
|
||||
else:
|
||||
CHANNEL_AXIS = 1
|
||||
ROW_AXIS = 2
|
||||
COL_AXIS = 3
|
||||
|
||||
|
||||
def _get_block(identifier):
|
||||
if isinstance(identifier, six.string_types):
|
||||
res = globals().get(identifier)
|
||||
if not res:
|
||||
raise ValueError('Invalid {}'.format(identifier))
|
||||
return res
|
||||
return identifier
|
||||
|
||||
|
||||
class ResnetBuilder(object):
|
||||
@staticmethod
|
||||
def build(input_shape, num_outputs, block_fn, repetitions):
|
||||
"""Builds a custom ResNet like architecture.
|
||||
|
||||
Args:
|
||||
input_shape: The input shape in the form (nb_channels, nb_rows, nb_cols)
|
||||
num_outputs: The number of outputs at final softmax layer
|
||||
block_fn: The block function to use. This is either `basic_block` or `bottleneck`.
|
||||
The original paper used basic_block for layers < 50
|
||||
repetitions: Number of repetitions of various block units.
|
||||
At each block unit, the number of filters are doubled and the input size is halved
|
||||
|
||||
Returns:
|
||||
The keras `Model`.
|
||||
"""
|
||||
_handle_dim_ordering()
|
||||
if len(input_shape) != 3:
|
||||
raise Exception("Input shape should be a tuple (nb_channels, nb_rows, nb_cols)")
|
||||
|
||||
# Permute dimension order if necessary
|
||||
if K.image_dim_ordering() == 'tf':
|
||||
input_shape = (input_shape[1], input_shape[2], input_shape[0])
|
||||
|
||||
# Load function from str if needed.
|
||||
block_fn = _get_block(block_fn)
|
||||
|
||||
input = Input(shape=input_shape)
|
||||
conv1 = _conv_bn_relu(filters=64, kernel_size=(7, 7), strides=(2, 2))(input)
|
||||
pool1 = MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding="same")(conv1)
|
||||
|
||||
block = pool1
|
||||
filters = 64
|
||||
for i, r in enumerate(repetitions):
|
||||
block = _residual_block(block_fn, filters=filters, repetitions=r, is_first_layer=(i == 0))(block)
|
||||
filters *= 2
|
||||
|
||||
# Last activation
|
||||
block = _bn_relu(block)
|
||||
|
||||
# Classifier block
|
||||
block_shape = K.int_shape(block)
|
||||
pool2 = AveragePooling2D(pool_size=(block_shape[ROW_AXIS], block_shape[COL_AXIS]),
|
||||
strides=(1, 1))(block)
|
||||
flatten1 = Flatten()(pool2)
|
||||
dense = Dense(units=num_outputs, kernel_initializer="he_normal",
|
||||
activation="softmax")(flatten1)
|
||||
|
||||
model = Model(inputs=input, outputs=dense)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def build_resnet_18(input_shape, num_outputs):
|
||||
return ResnetBuilder.build(input_shape, num_outputs, basic_block, [2, 2, 2, 2])
|
||||
|
||||
@staticmethod
|
||||
def build_resnet_34(input_shape, num_outputs):
|
||||
return ResnetBuilder.build(input_shape, num_outputs, basic_block, [3, 4, 6, 3])
|
||||
|
||||
@staticmethod
|
||||
def build_resnet_50(input_shape, num_outputs):
|
||||
return ResnetBuilder.build(input_shape, num_outputs, bottleneck, [3, 4, 6, 3])
|
||||
|
||||
@staticmethod
|
||||
def build_resnet_101(input_shape, num_outputs):
|
||||
return ResnetBuilder.build(input_shape, num_outputs, bottleneck, [3, 4, 23, 3])
|
||||
|
||||
@staticmethod
|
||||
def build_resnet_152(input_shape, num_outputs):
|
||||
return ResnetBuilder.build(input_shape, num_outputs, bottleneck, [3, 8, 36, 3])
|
||||
229
icenet/util.py
Normal file
229
icenet/util.py
Normal file
@ -0,0 +1,229 @@
|
||||
import os
|
||||
import json
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
from itertools import cycle
|
||||
from skimage import transform
|
||||
from sklearn.externals import joblib
|
||||
from sklearn.metrics import log_loss
|
||||
|
||||
|
||||
def binary_crossentropy(y_true, y_pred):
|
||||
return log_loss(y_true.flatten(), y_pred.flatten())
|
||||
|
||||
|
||||
def get_bands(band1, band2):
|
||||
return np.dstack((
|
||||
np.array(band1).reshape(75, 75),
|
||||
np.array(band2).reshape(75, 75)
|
||||
))
|
||||
|
||||
|
||||
def get_angle(a):
|
||||
if a == 'na':
|
||||
return None
|
||||
else:
|
||||
return a
|
||||
|
||||
|
||||
def get_label(l):
|
||||
if l is None:
|
||||
return l
|
||||
else:
|
||||
return int(l)
|
||||
|
||||
|
||||
def unpack(samples):
|
||||
images, angles, labels = zip(*[
|
||||
(get_bands(s['band_1'], s['band_2']),
|
||||
get_angle(s['inc_angle']),
|
||||
get_label(s.get('is_iceberg')))
|
||||
for s in samples
|
||||
])
|
||||
return images, angles, labels
|
||||
|
||||
|
||||
def load_samples(datadir, filename):
|
||||
with open(os.path.join(datadir, filename)) as f:
|
||||
samples = json.load(f)
|
||||
return samples
|
||||
|
||||
|
||||
def train_dev_split(samples, split, shuffle=True):
|
||||
split_idx = int(split*len(samples))
|
||||
if shuffle:
|
||||
random.seed(0)
|
||||
random.shuffle(samples)
|
||||
return (samples[0:split_idx], samples[split_idx:])
|
||||
|
||||
|
||||
def write_preds(datadir, filename, samples, y_pred):
|
||||
with open(os.path.join(datadir, 'predictions', filename), 'w') as f:
|
||||
f.write('id,is_iceberg\n')
|
||||
f.write('\n'.join([
|
||||
'%s,%.6f' % (samples[i].get('id'), y_pred[i][0])
|
||||
for i in range(len(samples))
|
||||
]))
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def model_fp(datadir, filename):
|
||||
return os.path.join(datadir, 'params', filename)
|
||||
|
||||
|
||||
def save_minmax(datadir, filename, minmax):
|
||||
joblib.dump(minmax, os.path.join(datadir, 'params', filename))
|
||||
|
||||
|
||||
def load_minmax(datadir, filename):
|
||||
return joblib.load(os.path.join(datadir, 'params', filename))
|
||||
|
||||
|
||||
def get_minmax(samples):
|
||||
# Extract global mins/max' for normalization
|
||||
images, angles, labels = unpack(samples)
|
||||
b1_min = min([im[:, :, 0].min() for im in images])
|
||||
b1_max = max([im[:, :, 0].max() for im in images])
|
||||
b2_min = min([im[:, :, 1].min() for im in images])
|
||||
b2_max = max([im[:, :, 1].max() for im in images])
|
||||
a_min = min(angles, key=lambda x: x or 1e9)
|
||||
a_max = max(angles, key=lambda x: x or -1e9)
|
||||
print("Band 1 min/max: %.2f, %.2f" % (b1_min, b1_max))
|
||||
print("Band 2 min/max: %.2f, %.2f" % (b2_min, b2_max))
|
||||
print("Angles min/max: %.2f, %.2f" % (a_min, a_max))
|
||||
return (b1_min, b1_max, b2_min, b2_max, a_min, a_max)
|
||||
|
||||
|
||||
def base_cnn_generator(samples, minmax, batch_size, verbose=False):
|
||||
img_dim = 75
|
||||
window = 28
|
||||
r = int(window/2)
|
||||
padding = int(window/np.sqrt(2))
|
||||
if verbose:
|
||||
print("window is %d px" % window)
|
||||
print("padding is %d px" % padding)
|
||||
|
||||
# Fuck the angles.
|
||||
images, _, labels = unpack(samples)
|
||||
b1_min, b1_max, b2_min, b2_max, _, _ = minmax
|
||||
|
||||
# Yield batches forever, cycling the samples with augmentation:
|
||||
# Random rotation and crop, and random Bernoulli mirroring.
|
||||
batch_images = []
|
||||
batch_labels = []
|
||||
for image, label in cycle(zip(images, labels)):
|
||||
# Scale each band independently
|
||||
img = np.dstack((
|
||||
(image[:, :, 0] - b1_min) / (b1_max - b1_min),
|
||||
(image[:, :, 1] - b2_min) / (b2_max - b2_min),
|
||||
))
|
||||
|
||||
# Save the maximum of both bands within this image
|
||||
b1_peak = img[:, :, 0].max()
|
||||
b2_peak = img[:, :, 1].max()
|
||||
|
||||
# Rotate around random midpoint
|
||||
row_mid = random.randint(padding, img_dim-padding-1)
|
||||
col_mid = random.randint(padding, img_dim-padding-1)
|
||||
if verbose:
|
||||
print("\nSample %d" % (len(batch_labels)+1))
|
||||
print("row/col mid: %d/%d" % (row_mid, col_mid))
|
||||
print("Mid values: %.2f, %.2f" % (img[row_mid, col_mid, 0], img[row_mid, col_mid, 1]))
|
||||
|
||||
# NOTE: sklearn.transform.rotate behaves incorrectly when using kwargs 'center'
|
||||
# Clip to mid with padding before rotating.
|
||||
img = img[row_mid-padding:row_mid+padding, col_mid-padding:col_mid+padding, :]
|
||||
rot = random.random() * 360 - 180
|
||||
img = transform.rotate(img, rot, order=1)
|
||||
if verbose:
|
||||
print("Rotation: %.2f deg" % rot)
|
||||
print("Mid values: %.2f, %.2f" % (img[padding, padding, 0], img[padding, padding, 1]))
|
||||
img = img[padding-r:padding+r, padding-r:padding+r, :]
|
||||
|
||||
# Mirror
|
||||
if random.randint(0, 1) and False:
|
||||
img = np.fliplr(img)
|
||||
if verbose:
|
||||
print("Sample was mirrored")
|
||||
|
||||
# Label by checking that subimage contains maximum of either band
|
||||
# If the subimage contains a max value that is within 95% of the image
|
||||
# max, consider it to contain the feature.
|
||||
sub_b1_peak = img[:, :, 0].max()
|
||||
sub_b2_peak = img[:, :, 1].max()
|
||||
l = int(sub_b1_peak/b1_peak > 0.95 and
|
||||
sub_b2_peak/b2_peak > 0.95) * (label + 1)
|
||||
if verbose:
|
||||
print("Label is %d" % l)
|
||||
categorical_label = tuple(int(i==l) for i in range(3))
|
||||
|
||||
batch_images.append(img)
|
||||
batch_labels.append(categorical_label)
|
||||
if len(batch_labels) == batch_size:
|
||||
yield (
|
||||
np.array(batch_images, dtype=np.float32),
|
||||
np.array(batch_labels, dtype=np.float32)
|
||||
)
|
||||
batch_images = []
|
||||
batch_labels = []
|
||||
|
||||
|
||||
def augment_image(img, mirror, rots):
|
||||
if mirror:
|
||||
img = np.fliplr(img)
|
||||
img = np.rot90(img, k=rots)
|
||||
return img
|
||||
|
||||
|
||||
def icenet_generator(samples, minmax, batch_size, crop_offset=3,
|
||||
augment=True, verbose=False):
|
||||
img_dim = 75
|
||||
mid_x = mid_y = 75.0 / 2
|
||||
crop_dim = img_dim - 2*crop_offset
|
||||
window = 28
|
||||
r = int(window/2)
|
||||
|
||||
images, _, labels = unpack(samples)
|
||||
b1_min, b1_max, b2_min, b2_max, _, _ = minmax
|
||||
|
||||
# Yield batches forever
|
||||
# A batch consists of an input-output tuple
|
||||
# ([X_image_sec1, X_image_sec2, ... , X_image_sec9], Y_labels)
|
||||
# where the input image is split over 9 overlapping sections, starting
|
||||
# from upper left in row-major order
|
||||
batch_image_sections = [[] for _ in range(9)]
|
||||
batch_labels = []
|
||||
for image, label in cycle(zip(images, labels)):
|
||||
# Scale each band independently
|
||||
img = np.dstack((
|
||||
(image[:, :, 0] - b1_min) / (b1_max - b1_min),
|
||||
(image[:, :, 1] - b2_min) / (b2_max - b2_min),
|
||||
))
|
||||
|
||||
# Crop with random offset from midpoint
|
||||
row_offset = crop_offset * (2*random.random() - 1)
|
||||
col_offset = crop_offset * (2*random.random() - 1)
|
||||
if not augment:
|
||||
row_offset = col_offset = 0
|
||||
row = int(mid_y + row_offset - crop_dim/2)
|
||||
col = int(mid_x + col_offset - crop_dim/2)
|
||||
img = img[row:row+crop_dim, col:col+crop_dim, :]
|
||||
|
||||
if augment:
|
||||
img = augment_image(img, random.randint(0, 1), random.randint(0, 3))
|
||||
|
||||
# Append each section of the image
|
||||
for i, ridx in enumerate([r, int(mid_y), crop_dim-r]):
|
||||
for j, cidx in enumerate([r, int(mid_x), crop_dim-r]):
|
||||
batch_image_sections[i*3+j].append(img[ridx-r:ridx+r, cidx-r:cidx+r, :])
|
||||
|
||||
batch_labels.append(label)
|
||||
if len(batch_labels) == batch_size:
|
||||
yield (
|
||||
[np.array(sec_imgs, dtype=np.float32) for sec_imgs in batch_image_sections],
|
||||
np.array(batch_labels, dtype=np.float32)
|
||||
)
|
||||
batch_image_sections = [[] for _ in range(9)]
|
||||
batch_labels = []
|
||||
|
||||
104
plot.py
Executable file
104
plot.py
Executable file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plot training/test data. Channels appear side by side."""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from icenet import util
|
||||
|
||||
|
||||
def plot_sample(outdir, sample, minmax, interactive=False):
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, sharex='col', sharey='row')
|
||||
fig.suptitle('\n'.join([
|
||||
"Id: %s (original)" % sample.get('id'),
|
||||
"Iceberg? %s" % sample.get('is_iceberg', '-'),
|
||||
"Incident angle: %s" % repr(sample.get('inc_angle'))])
|
||||
)
|
||||
|
||||
b1_min, b1_max, b2_min, b2_max, _, _ = minmax
|
||||
b1 = (np.array(sample['band_1']).reshape(75, 75) - b1_min) / (b1_max - b1_min)
|
||||
b2 = (np.array(sample['band_2']).reshape(75, 75) - b2_min) / (b2_max - b2_min)
|
||||
ax1.imshow(b1, vmin=0.2, vmax=0.8, aspect='auto')
|
||||
ax1.set_title("HH")
|
||||
ax2.imshow(b2, vmin=0.2, vmax=0.8, aspect='auto')
|
||||
ax2.set_title("HV")
|
||||
|
||||
#fig.tight_layout()
|
||||
fig.subplots_adjust(top=0.80)
|
||||
if interactive:
|
||||
plt.show()
|
||||
else:
|
||||
fig.savefig(os.path.join(outdir, "%s.png" % sample['id']))
|
||||
plt.close('all')
|
||||
|
||||
|
||||
def plot_angle_hist(outdir, samples, interactive=False):
|
||||
angles = [s['inc_angle'] for s in samples if s['inc_angle'] != 'na']
|
||||
|
||||
hist, bins = np.histogram(angles, bins=50)
|
||||
width = 0.7 * (bins[1] - bins[0])
|
||||
center = (bins[:-1] + bins[1:]) / 2
|
||||
|
||||
fig = plt.figure()
|
||||
plt.bar(center, hist, align='center', width=width)
|
||||
plt.title("%d/%d valid angles" % (len(angles), len(samples)))
|
||||
if interactive:
|
||||
plt.show()
|
||||
else:
|
||||
fig.savefig(os.path.join(outdir, "angle_hist.png"))
|
||||
plt.close('all')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
required=False,
|
||||
help="Output directory for samples."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
type=int,
|
||||
required=False,
|
||||
help="Number of samples to plot (randomized)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
required=False,
|
||||
action='store_true',
|
||||
help="Show plots (and don't store)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'samples_file',
|
||||
help="JSON file with samples"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
outdir = args.o or '.'
|
||||
if not os.path.isdir(outdir):
|
||||
raise Exception("Output directory does not exist")
|
||||
|
||||
print("Loading samples ...")
|
||||
with open(args.samples_file) as f:
|
||||
samples = json.load(f)
|
||||
print("%d samples in set" % len(samples))
|
||||
|
||||
if args.n:
|
||||
print("Pick %d random samples ..." % args.n)
|
||||
random.shuffle(samples)
|
||||
samples = samples[0:args.n]
|
||||
|
||||
minmax = util.get_minmax(samples)
|
||||
for i, s in enumerate(samples):
|
||||
print("Plotting sample %d/%d" % (i+1, len(samples)))
|
||||
plot_sample(outdir, s, minmax, interactive=args.i or False)
|
||||
|
||||
print("Plot angle histogram ..")
|
||||
plot_angle_hist(outdir, samples, interactive=args.i or False)
|
||||
|
||||
print("Done.")
|
||||
133
plot_augmented.py
Executable file
133
plot_augmented.py
Executable file
@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plot image produced by data generators in icenet/utils.py"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from icenet import util
|
||||
from plot import plot_sample
|
||||
from itertools import islice
|
||||
|
||||
|
||||
def plot_base_cnn_samples(outdir, samples, interactive=False):
|
||||
m = len(samples)
|
||||
minmax = util.get_minmax(samples)
|
||||
imgs, _ = list(islice(util.base_cnn_generator(
|
||||
samples, minmax, m, shuffle=False, verbose=True
|
||||
), 1))[0]
|
||||
for i in range(m):
|
||||
print("Plotting sample %d/%d" % (i+1, m))
|
||||
sample = samples[i]
|
||||
img = imgs[i, :, :, :]
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, sharex='col', sharey='row')
|
||||
fig.suptitle('\n'.join([
|
||||
"Id: %s (rotated and cropped)" % sample.get('id'),
|
||||
"Iceberg? %s" % sample.get('is_iceberg', '-'),
|
||||
"Incident angle: %s" % repr(sample.get('inc_angle'))])
|
||||
)
|
||||
|
||||
ax1.imshow(img[:, :, 0], vmin=0.2, vmax=0.8, aspect='auto')
|
||||
ax1.set_title("HH")
|
||||
ax2.imshow(img[:, :, 1], vmin=0.2, vmax=0.8, aspect='auto')
|
||||
ax2.set_title("HV")
|
||||
|
||||
#fig.tight_layout()
|
||||
fig.subplots_adjust(top=0.80)
|
||||
plot_sample(outdir, sample, minmax, interactive=interactive)
|
||||
if not interactive:
|
||||
fig.savefig(os.path.join(outdir, "%s_base.png" % sample['id']))
|
||||
plt.close('all')
|
||||
|
||||
|
||||
def plot_icenet_samples(outdir, samples, interactive=False):
|
||||
m = len(samples)
|
||||
minmax = util.get_minmax(samples)
|
||||
sections, y = list(islice(util.icenet_generator(
|
||||
samples, minmax, m, augment=False, verbose=True
|
||||
), 1))[0]
|
||||
for i in range(m):
|
||||
print("Plotting sample %d/%d" % (i+1, m))
|
||||
sample = samples[i]
|
||||
|
||||
for b in range(2):
|
||||
fig, axes = plt.subplots(3, 3, sharex='col', sharey='row')
|
||||
fig.suptitle('\n'.join([
|
||||
"Id: %s (rotated and cropped, band %d)" % (sample.get('id'), b+1),
|
||||
"Iceberg? %s" % sample.get('is_iceberg', '-'),
|
||||
"Incident angle: %s" % repr(sample.get('inc_angle'))])
|
||||
)
|
||||
|
||||
for ridx in range(3):
|
||||
for cidx in range(3):
|
||||
axes[ridx, cidx].imshow(
|
||||
sections[ridx*3+cidx][i, :, :, b],
|
||||
vmin=0.2,
|
||||
vmax=0.8,
|
||||
aspect='auto'
|
||||
)
|
||||
|
||||
fig.subplots_adjust(top=0.80)
|
||||
if not interactive:
|
||||
fig.savefig(os.path.join(outdir, "%s_icenet_b%d.png" % (sample['id'], b)))
|
||||
|
||||
plot_sample(outdir, sample, minmax, interactive=interactive)
|
||||
plt.close('all')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
required=False,
|
||||
help="Output directory for samples."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-g',
|
||||
required=True,
|
||||
choices=['base', 'icenet'],
|
||||
help="Generator to use (base|icenet)."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
type=int,
|
||||
required=False,
|
||||
help="Number of samples to plot (randomized)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
required=False,
|
||||
action='store_true',
|
||||
help="Show plots (and don't store)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'samples_file',
|
||||
help="JSON file with samples"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
outdir = args.o or '.'
|
||||
if not os.path.isdir(outdir):
|
||||
raise Exception("Output directory does not exist")
|
||||
|
||||
print("Loading samples ...")
|
||||
with open(args.samples_file) as f:
|
||||
samples = json.load(f)
|
||||
print("%d samples in set" % len(samples))
|
||||
|
||||
if args.n:
|
||||
print("Pick %d random samples ..." % args.n)
|
||||
random.shuffle(samples)
|
||||
samples = samples[0:args.n]
|
||||
|
||||
if args.g == 'base':
|
||||
plot_base_cnn_samples(outdir, samples, interactive=args.i or False)
|
||||
else:
|
||||
plot_icenet_samples(outdir, samples, interactive=args.i or False)
|
||||
|
||||
print("Done.")
|
||||
29
run_model.py
Executable file
29
run_model.py
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a model."""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from icenet import MODELS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
required=True,
|
||||
help="Directory containing {train,test}.json."
|
||||
)
|
||||
parser.add_argument(
|
||||
'target_fn',
|
||||
help=("Target <module>.<fn> in the icenet package. "
|
||||
"The function must take the data directory as argument.")
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not os.path.isdir(args.d):
|
||||
raise Exception("Output directory does not exist")
|
||||
|
||||
model, fn = args.target_fn.split('.')
|
||||
print("Running model %s ..." % model)
|
||||
fn = getattr(MODELS[model], fn)
|
||||
fn(args.d)
|
||||
print("Done.")
|
||||
5
run_model_docker.sh
Executable file
5
run_model_docker.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
docker build -t iceberg:$(cat VERSION) .
|
||||
docker run --name iceberg --rm -v $(pwd)/data:/data iceberg:$(cat VERSION) $@
|
||||
11
show_predicted.sh
Executable file
11
show_predicted.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Args:
|
||||
# path to predictions (CSV)
|
||||
# path to images
|
||||
|
||||
tail -n+2 $1 | shuf | while read -r i; do
|
||||
ID=$(echo $i | cut -d ',' -f1)
|
||||
PRED=$(echo $i | cut -d ',' -f2)
|
||||
echo "ID $ID: is_iceberg = $PRED" && gwenview $2/$ID.png 2>/dev/null
|
||||
done
|
||||
Loading…
x
Reference in New Issue
Block a user