LIVIAETS / boundary-loss

Official code for "Boundary loss for highly unbalanced segmentation", runner-up for best paper award at MIDL 2019. Extended version in MedIA, volume 67, January 2021.
https://doi.org/10.1016/j.media.2020.101851
MIT License
652 stars 97 forks source link

Question on SurfaceLoss #11

Closed WMeixiang closed 5 years ago

WMeixiang commented 5 years ago

Thanks for sharing a very good idea. I am interested in the surface loss function.

(1): In the SurfaceLoss(), the dist_maps is Tensor, while dist_maps is from def one_hot2dist(seg: np.ndarray) -> np.ndarray:, the format of input and output is ndarray, how to convert the format of data? This question confused me several days, I want to rewrite SurfaceLoss in keras, The following is the code I am rewriting:

def one_hot2dist(seg: np.ndarray) -> np.ndarray:
    C: int = len(seg)
    res = np.zeros_like(seg)
    for c in range(C):
        posmask = seg[c].astype(np.bool)
        if posmask.any():
            negmask = ~posmask
            res[c] = distance(negmask) * negmask - (distance(posmask) - 1) * posmask
    return res

def SurfaceLoss(y_true,y_pred):            # y_true, y_pred: tensorflow tensor
    with tf.Session() as sess:
        y_true_Numpy = y_true.eval()
    dist_maps = one_hot2dist(y_true_Numpy)
    dist_maps_tensor = tf.convert_to_tensor(dist_maps)
    #assert simplex(y_pred)
    #assert not one_hot(dist_maps)
    loss =  K.sum(dist_maps_tensor * y_pred)      
    return loss

if I use generalized_dice_loss as cost, my code is ok, but if I use SurfaceLoss, there are some error:

InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'conv2d_19_target' with dtype float and shape [?,?,?,?]
     [[node conv2d_19_target (defined at /home/mx/anaconda3/lib/python3.6/site-packages/keras/backend/tensorflow_backend.py:517)  = Placeholder[dtype=DT_FLOAT, shape=[?,?,?,?], _device="/job:localhost/replica:0/task:0/device:GPU:0"]()]]
     [[{{node conv2d_19_target/_103}} = _Recv[client_terminated=false, recv_device="/job:localhost/replica:0/task:0/device:CPU:0", send_device="/job:localhost/replica:0/task:0/device:GPU:0", send_device_incarnation=1, tensor_name="edge_4_conv2d_19_target", tensor_type=DT_FLOAT, _device="/job:localhost/replica:0/task:0/device:CPU:0"]()]]

---> I guess the problem is the dimension problem about output, I try to modify, but it always have this problem, could you help me?

(2): your final_loss is formula(6) in your paper midl, combine the generalized_dice_loss and surfaceloss, but I did not find final_loss in your main.py, how to operate?

(3): In formula(6), alpha is dynamic, is related to epoch, how to generate alpha? In your project, I didn't find the definition of function about alpha

(4): For two classifications, the activation function of final layer: softmax, sigmoid. what the difference between softmax and sigmoid? when I used generalized_dice_loss as cost in my code, the sigmoid function perform better than softmax, I have no idea why? Looking forward to your reply.

HKervadec commented 5 years ago

(1) I am not used to Tensorflow or Keras, so is it difficult for me to help you. Especially since the paradigm is slightly different (where stuff runs in a session). Are you sure the two are on the same device ? The _device variable seems different in your error message.

As you can see, the results of one_hot2dist will be the same shape as its input. But you could add an assertion at the end just to be sure:

res = np.zeros_like(seg)
...
assert res.shape == seg.shape
return res

If this pass, you should also make sure that y_true and y_pred have the same shape (guess you will have to use tf.assert for that, don't know the details). Other than that, I think it would be a problem different than the shape. Perhaps because the y_pred are floats, while the distance maps are doubles (I think NumPy defaults to float64 and not float32 as many deep learning frameworks).

(2): Yes it is not obvious at first. I actually give a list of losses to the main script, which are then summed:

def setup(...):
    ...
    losses = eval(args.losses)
    loss_fns: List[Callable] = []
    for loss_name, loss_params, _, _, fn, _ in losses:
        loss_class = getattr(__import__('losses'), loss_name)
        loss_fns.append(loss_class(**loss_params, fn=fn))

    loss_weights: List[float] = map_(itemgetter(5), losses)
    ...

def do_epoch(...):
    ...

    for j, data in enumerate(loader):
        ...

        # Forward
        pred_logits: Tensor = net(image)
        pred_probs: Tensor = F.softmax(pred_logits, dim=1)

        assert len(bounds) == len(loss_fns) == len(loss_weights)
        ziped = zip(loss_fns, labels, loss_weights, bounds)

        # Apply each loss, creating a list of loss values
        losses = [w * loss_fn(pred_probs, label, bound) for loss_fn, label, w, bound in ziped]

        # Then sum them
        loss = reduce(add, losses)
        assert loss.shape == (), loss.shape

        # Backward
        if optimizer:
            loss.backward()
            optimizer.step()

        ...
    ...

While this is more complicated at first, it gives much more flexibility then to experiment and avoid code duplication.

(3): I update the loss weights in the schedulers:

optimizer, loss_fns, loss_weights = scheduler(i, optimizer, loss_fns, loss_weights)

where scheduler is one of the strategies defined in the scheduler.py file. There is --schedule, --scheduler, and --scheduler_params to define what you wan to use. Many examples of options can be found in the makefile

(4) Sigmoid or softmax, that is actually a hyper-parameter (just as the network architecture and the activation functions inside the network). So for some problem one will be better, and on other tasks it will be the opposite. The main difference will be wrt the gradient of the loss.

In addition, I define the SurfaceLoss by 'def ', rather than by 'class' . Is this different?

No, that is the same. I use a class (where I define the __call__ function) so I can easily add parameters to the loss (which is the case for some of my other projects). But in this case, and function and a callable object are effectively the same.

WMeixiang commented 5 years ago

Thank you very much for your detailed reply. (1)Through experiments, I know that y_true is a placeholder, there is no specific value. So, Is the one_hot2dist function applied to the label data corresponding to image used to train? (2) If I use one_hot2dist in your code, applied to the imgs_mask_train, then it works, but I have an question,If I simply run the boundary loss, my metric [dice_coef] did not improve during training?instead,it decreased at first, then stabilize at a value. Besides, the value of the boundary loss fluctuated between 95 and 96, also did not decrease? I have no idea why? (3) I seriously read your main.py, You update the weights in each loop. Maybe the framework is different, I also try to use the loop to update the weights or write a custom callback, but it always report an error. (4) Since I don't have the data corresponding to your code, I could’t reproduce your results. I want to use your code to run my existing data. My data is also in numpy format. I don't know much about makefiles. I don't know how to modify the makefile to adapt to my data. Besides the makefile, is there necessary to modify other script? Could you help me?

HKervadec commented 5 years ago

Hey,

(1): Yes, we apply one_hot2dist to the labels of the image.

(2): The specifics (boundary loss weight, other loss to use if you want one, and other hyper-parameters) will be dependent of the task, as for many machine learning applications. You will have to experiment to see what fits your applications.

(3): Yeah that is how we do things in Pytorch, but I won't be able to help you on another framework. Usually they should work in a similar fashion.

(4): You can either get the data yourself (links in https://github.com/LIVIAETS/surface-loss/blob/master/data/wmh.lineage and https://github.com/LIVIAETS/surface-loss/blob/master/data/ISLES.lineage), or adapt to your data. Since the datasets used in the paper are multi-modal, I had to save the data as npy files, with the shape modality x width x height for the image, and width x height for the labels . So I think the main modification for you will be to replace the location of the data in the makefiles (you can do that with a search and replace, that is what I do when I use a new dataset). You can also refer to my other reply about makefiles https://github.com/LIVIAETS/surface-loss/issues/10#issuecomment-492269482

Take a look as well to the readme, where I describe how I structure the dataset folders (https://github.com/LIVIAETS/surface-loss section datascheme/dataset)

Currently, my code use the torch.tensor and gt_transform transforms for the image and labels, respectively:

# torch.tensor: simply copy the numpy data into a Tensor, without any modification

# gt_transform: go from class_number (shape wh) to one_hot encoding (shape cwh)
gt_transform = transforms.Compose([
    lambda img: np.array(img)[np.newaxis, ...],
    lambda nd: torch.tensor(nd, dtype=torch.int64),
    partial(class2one_hot, C=n_class),
    itemgetter(0)
]) 

If your data is different, you will either need to change the transform used, or to pre-process your data ; that is up to you.

Hope that helps.

WMeixiang commented 5 years ago

Thank you very much for your reply. I will try again.

Best wishes,

Meixiang Huang

-----原始邮件----- 发件人:"Hoel KERVADEC" notifications@github.com 发送时间:2019-06-03 23:22:42 (星期一) 收件人: LIVIAETS/surface-loss surface-loss@noreply.github.com 抄送: WMeixiang 11735032@zju.edu.cn, Author author@noreply.github.com 主题: Re: [LIVIAETS/surface-loss] Question on SurfaceLoss (#11)

Hey,

(1): Yes, we apply one_hot2dist to the labels of the image.

(2): The specifics (boundary loss weight, other loss to use if you want one, and other hyper-parameters) will be dependent of the task, as for many machine learning applications. You will have to experiment to see what fits your applications.

(3): Yeah that is how we do things in Pytorch, but I won't be able to help you on another framework. Usually they should work in a similar fashion.

(4): You can either get the data yourself (links in https://github.com/LIVIAETS/surface-loss/blob/master/data/wmh.lineage and https://github.com/LIVIAETS/surface-loss/blob/master/data/ISLES.lineage), or adapt to your data. Since the datasets used in the paper are multi-modal, I had to save the data as npy files, with the shape modality x width x height for the image, and width x height for the labels . So I think the main modification for you will be to replace the location of the data in the makefiles (you can do that with a search and replace, that is what I do when I use a new dataset). You can also refer to my other reply about makefiles #10 (comment)

Take a look as well to the readme, where I describe how I structure the dataset folders (https://github.com/LIVIAETS/surface-loss section datascheme/dataset)

Currently, my code use the torch.tensor and gt_transform transforms for the image and labels, respectively:

torch.tensor: simply copy the numpy data into a Tensor, without any modification# gt_transform: go from class_number (shape wh) to one_hot encoding (shape cwh) gt_transform = transforms.Compose([ lambdaimg: np.array(img)[np.newaxis, ...], lambdand: torch.tensor(nd, dtype=torch.int64),

partial(class2one_hot, C=n_class),
itemgetter(0)

])

If your data is different, you will either need to change the transform used, or to pre-process your data ; that is up to you.

Hope that helps.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or mute the thread.

AmericaBG commented 4 years ago

Thank you very much for your reply. I will try again. Best wishes, Meixiang Huang -----原始邮件----- 发件人:"Hoel KERVADEC" notifications@github.com 发送时间:2019-06-03 23:22:42 (星期一) 收件人: LIVIAETS/surface-loss surface-loss@noreply.github.com 抄送: WMeixiang 11735032@zju.edu.cn, Author author@noreply.github.com 主题: Re: [LIVIAETS/surface-loss] Question on SurfaceLoss (#11) Hey, (1): Yes, we apply one_hot2dist to the labels of the image. (2): The specifics (boundary loss weight, other loss to use if you want one, and other hyper-parameters) will be dependent of the task, as for many machine learning applications. You will have to experiment to see what fits your applications. (3): Yeah that is how we do things in Pytorch, but I won't be able to help you on another framework. Usually they should work in a similar fashion. (4): You can either get the data yourself (links in https://github.com/LIVIAETS/surface-loss/blob/master/data/wmh.lineage and https://github.com/LIVIAETS/surface-loss/blob/master/data/ISLES.lineage), or adapt to your data. Since the datasets used in the paper are multi-modal, I had to save the data as npy files, with the shape modality x width x height for the image, and width x height for the labels . So I think the main modification for you will be to replace the location of the data in the makefiles (you can do that with a search and replace, that is what I do when I use a new dataset). You can also refer to my other reply about makefiles #10 (comment) Take a look as well to the readme, where I describe how I structure the dataset folders (https://github.com/LIVIAETS/surface-loss section datascheme/dataset) Currently, my code use the torch.tensor and gt_transform transforms for the image and labels, respectively: # torch.tensor: simply copy the numpy data into a Tensor, without any modification# gt_transform: go from class_number (shape wh) to one_hot encoding (shape cwh) gt_transform = transforms.Compose([ lambdaimg: np.array(img)[np.newaxis, ...], lambdand: torch.tensor(nd, dtype=torch.int64), partial(class2one_hot, C=n_class), itemgetter(0) ]) If your data is different, you will either need to change the transform used, or to pre-process your data ; that is up to you. Hope that helps. — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or mute the thread.

Hi WMeixiang!

I'm interested in surface loss keras implementation too! Did you get it work??

Thank you very much in advance!

WMeixiang commented 4 years ago

Hey, sorry for the late reply!

There may be something wrong with my code when I use surface loss. You could try to implement one_hot2dist() seriously with your data, this function is the key point.

2019-10-28 23:48:26"América Bueno Gómez" notifications@github.com写道:

Thank you very much for your reply. I will try again. Best wishes, Meixiang Huang … -----原始邮件----- 发件人:"Hoel KERVADEC" notifications@github.com 发送时间:2019-06-03 23:22:42 (星期一) 收件人: LIVIAETS/surface-loss surface-loss@noreply.github.com 抄送: WMeixiang 11735032@zju.edu.cn, Author author@noreply.github.com 主题: Re: [LIVIAETS/surface-loss] Question on SurfaceLoss (#11) Hey, (1): Yes, we apply one_hot2dist to the labels of the image. (2): The specifics (boundary loss weight, other loss to use if you want one, and other hyper-parameters) will be dependent of the task, as for many machine learning applications. You will have to experiment to see what fits your applications. (3): Yeah that is how we do things in Pytorch, but I won't be able to help you on another framework. Usually they should work in a similar fashion. (4): You can either get the data yourself (links in https://github.com/LIVIAETS/surface-loss/blob/master/data/wmh.lineage and https://github.com/LIVIAETS/surface-loss/blob/master/data/ISLES.lineage), or adapt to your data. Since the datasets used in the paper are multi-modal, I had to save the data as npy files, with the shape modality x width x height for the image, and width x height for the labels . So I think the main modification for you will be to replace the location of the data in the makefiles (you can do that with a search and replace, that is what I do when I use a new dataset). You can also refer to my other reply about makefiles #10 (comment) Take a look as well to the readme, where I describe how I structure the dataset folders (https://github.com/LIVIAETS/surface-loss section datascheme/dataset) Currently, my code use the torch.tensor and gt_transform transforms for the image and labels, respectively: # torch.tensor: simply copy the numpy data into a Tensor, without any modification# gt_transform: go from class_number (shape wh) to one_hot encoding (shape cwh) gt_transform = transforms.Compose([ lambdaimg: np.array(img)[np.newaxis, ...], lambdand: torch.tensor(nd, dtype=torch.int64), partial(class2one_hot, C=n_class), itemgetter(0) ]) If your data is different, you will either need to change the transform used, or to pre-process your data ; that is up to you. Hope that helps. — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or mute the thread.

Hi WMeixiang!

I'm interested in surface loss keras implementation too! Did you get it work??

Thank you very much in advance!

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or unsubscribe.