【问题标题】:CTC loss goes down and stopsCTC损失下降并停止
【发布时间】:2018-09-04 02:55:17
【问题描述】:

我正在尝试训练一个验证码识别模型。模型细节是 resnet 预训练的 CNN 层 + 双向 LSTM + 全连接。它在 python 库captcha 生成的验证码上达到了 90% 的序列准确率。问题是这些生成的验证码似乎每个字符的位置相似。当我在字符之间随机添加空格时,模型不再起作用。所以我想知道LSTM是在学习过程中学习分割吗?然后我尝试使用 CTC 损失。起初,损失下降得很快。但它保持在16左右,之后没有明显下降。我尝试了不同层的 LSTM,不同数量的单元。 LSTM 的 2 层达到了较低的损失,但仍然没有收敛。 3 层就像 2 层。损失曲线:

#encoding:utf8
import os
import sys
import torch
import warpctc_pytorch
import traceback

import torchvision
from torch import nn, autograd, FloatTensor, optim
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torch.optim.lr_scheduler import MultiStepLR
from tensorboard import SummaryWriter
from pprint import pprint

from net.utils import decoder

from logging import getLogger, StreamHandler
logger = getLogger(__name__)
handler = StreamHandler(sys.stdout)
logger.addHandler(handler)

from dataset_util.utils import id_to_character
from dataset_util.transform import rescale, normalizer
from config.config import MAX_CAPTCHA_LENGTH, TENSORBOARD_LOG_PATH, MODEL_PATH


class CNN_RNN(nn.Module):
    def __init__(self, lstm_bidirectional=True, use_ctc=True, *args, **kwargs):
        super(CNN_RNN, self).__init__(*args, **kwargs)
        model_conv = torchvision.models.resnet18(pretrained=True)
        for param in model_conv.parameters():
            param.requires_grad = False

        modules = list(model_conv.children())[:-1]  # delete the last fc layer.
        for param in modules[8].parameters():
            param.requires_grad = True

        self.resnet = nn.Sequential(*modules)            # CNN with fixed parameters from resnet as feature extractor
        self.lstm_input_size = 512 * 2 * 2
        self.lstm_hidden_state_size = 512
        self.lstm_num_layers = 2
        self.chracter_space_length = 64
        self._lstm_bidirectional = lstm_bidirectional
        self._use_ctc = use_ctc
        if use_ctc:
            self._max_captcha_length = int(MAX_CAPTCHA_LENGTH * 2)
        else:
            self._max_captcha_length = MAX_CAPTCHA_LENGTH

        if lstm_bidirectional:
            self.lstm_hidden_state_size = self.lstm_hidden_state_size * 2           # so that hidden size for one direction in bidirection lstm is the same as vanilla lstm
            self.lstm = self.lstm = nn.LSTM(self.lstm_input_size, self.lstm_hidden_state_size // 2, dropout=0.5, bidirectional=True, num_layers=self.lstm_num_layers)
        else:
            self.lstm = nn.LSTM(self.lstm_input_size, self.lstm_hidden_state_size, dropout=0.5, bidirectional=False, num_layers=self.lstm_num_layers)  # dropout doen't work for one layer lstm

        self.ouput_to_tag = nn.Linear(self.lstm_hidden_state_size, self.chracter_space_length)
        self.tensorboard_writer = SummaryWriter(TENSORBOARD_LOG_PATH)
        # self.dropout_lstm = nn.Dropout()


    def init_hidden_status(self, batch_size):
        if self._lstm_bidirectional:
            self.hidden = (autograd.Variable(torch.zeros((self.lstm_num_layers * 2, batch_size, self.lstm_hidden_state_size // 2))),
                           autograd.Variable(torch.zeros((self.lstm_num_layers * 2, batch_size, self.lstm_hidden_state_size // 2)))) # number of layers, batch size, hidden dimention
        else:
            self.hidden = (autograd.Variable(torch.zeros((self.lstm_num_layers, batch_size, self.lstm_hidden_state_size))),
                           autograd.Variable(torch.zeros((self.lstm_num_layers, batch_size, self.lstm_hidden_state_size)))) # number of layers, batch size, hidden dimention


    def forward(self, image):
        '''
        :param image:  # batch_size, CHANNEL, HEIGHT, WIDTH
        :return:
        '''
        features = self.resnet(image)                 # [batch_size, 512, 2, 2]
        batch_size = image.shape[0]
        features = [features.view(batch_size, -1) for i in range(self._max_captcha_length)]
        features = torch.stack(features)
        self.init_hidden_status(batch_size)
        output, hidden = self.lstm(features, self.hidden)
        # output = self.dropout_lstm(output)
        tag_space = self.ouput_to_tag(output.view(-1, output.size(2)))      # [MAX_CAPTCHA_LENGTH * BATCH_SIZE, CHARACTER_SPACE_LENGTH]
        tag_space = tag_space.view(self._max_captcha_length, batch_size, -1)

        if not self._use_ctc:
            tag_score = F.log_softmax(tag_space, dim=2)             # [MAX_CAPTCHA_LENGTH, BATCH_SIZE, CHARACTER_SPACE_LENGTH]
        else:
            tag_score = tag_space

        return tag_score


    def train_net(self, data_loader, eval_data_loader=None, learning_rate=0.008, epoch_num=400):
        try:
            if self._use_ctc:
                loss_function = warpctc_pytorch.warp_ctc.CTCLoss()
            else:
                loss_function = nn.NLLLoss()

            # optimizer = optim.SGD(filter(lambda p: p.requires_grad, self.parameters()), momentum=0.9, lr=learning_rate)
            # optimizer = MultiStepLR(optimizer, milestones=[10,15], gamma=0.5)

            # optimizer = optim.Adadelta(filter(lambda p: p.requires_grad, self.parameters()))
            optimizer = optim.Adam(filter(lambda p: p.requires_grad, self.parameters()))
            self.tensorboard_writer.add_scalar("learning_rate", learning_rate)

            tensorbard_global_step=0
            if os.path.exists(os.path.join(TENSORBOARD_LOG_PATH, "resume_step")):
                with open(os.path.join(TENSORBOARD_LOG_PATH, "resume_step"), "r") as file_handler:
                    tensorbard_global_step = int(file_handler.read()) + 1

            for epoch_index, epoch in enumerate(range(epoch_num)):
                for index, sample in enumerate(data_loader):
                    optimizer.zero_grad()
                    input_image = autograd.Variable(sample["image"])        # batch_size, 3, 255, 255
                    tag_score = self.forward(input_image)

                    if self._use_ctc:
                        tag_score, target, tag_score_sizes, target_sizes = self._loss_preprocess_ctc(tag_score, sample)
                        loss = loss_function(tag_score, target, tag_score_sizes, target_sizes)
                        loss = loss / tag_score.size(1)

                    else:
                        target = sample["padded_label_idx"]
                        tag_score, target = self._loss_preprocess(tag_score, target)
                        loss = loss_function(tag_score, target)

                    print("Training loss: {}".format(float(loss)))
                    self.tensorboard_writer.add_scalar("training_loss", float(loss), tensorbard_global_step)
                    loss.backward()
                    optimizer.step()

                    if index % 250 == 0:
                        print(u"Processing batch: {} of {}, epoch: {}".format(index, len(data_loader), epoch_index))
                        self.evaluate(eval_data_loader, loss_function, tensorbard_global_step)

                    tensorbard_global_step += 1

                self.save_model(MODEL_PATH + "_epoch_{}".format(epoch_index))

        except KeyboardInterrupt:
            print("Exit for KeyboardInterrupt, save model")
            self.save_model(MODEL_PATH)

            with open(os.path.join(TENSORBOARD_LOG_PATH, "resume_step"), "w") as file_handler:
                file_handler.write(str(tensorbard_global_step))

        except Exception as excp:
            logger.error(str(excp))
            logger.error(traceback.format_exc())


    def predict(self, image):
        # TODO ctc version
        '''
        :param image: [batch_size, channel, height, width]
        :return:
        '''
        tag_score = self.forward(image)
        # TODO ctc
        # if self._use_ctc:
        #     tag_score = F.softmax(tag_score, dim=-1)
        #     decoder.decode(tag_score)

        confidence_log_probability, indexes = tag_score.max(2)

        predicted_labels = []
        for batch_index in range(indexes.size(1)):
            label = ""
            for character_index in range(self._max_captcha_length):
                if int(indexes[character_index, batch_index]) != 1:
                    label += id_to_character[int(indexes[character_index, batch_index])]
            predicted_labels.append(label)

        return predicted_labels, tag_score


    def predict_pil_image(self, pil_image):
        try:
            self.eval()
            processed_image = normalizer(rescale({"image": pil_image}))["image"].view(1, 3, 255, 255)
            result, tag_score = self.predict(processed_image)
            self.train()

        except Exception as excp:
            logger.error(str(excp))
            logger.error(traceback.format_exc())
            return [""], None

        return result, tag_score


    def evaluate(self, eval_dataloader, loss_function, step=0):
        total = 0
        sequence_correct = 0
        character_correct = 0
        character_total = 0
        loss_total = 0
        batch_size = eval_data_loader.batch_size
        true_predicted = {}
        self.eval()
        for sample in eval_dataloader:
            total += batch_size
            input_images = sample["image"]
            predicted_labels, tag_score = self.predict(input_images)

            for predicted, true_label in zip(predicted_labels, sample["label"]):
                if predicted == true_label:                  # dataloader is making label a list, use batch_size=1
                    sequence_correct += 1

                for index, true_character in enumerate(true_label):
                    character_total += 1
                    if index < len(predicted) and predicted[index] == true_character:
                        character_correct += 1

                true_predicted[true_label] = predicted

            if self._use_ctc:
                tag_score, target, tag_score_sizes, target_sizes = self._loss_preprocess_ctc(tag_score, sample)
                loss_total += float(loss_function(tag_score, target, tag_score_sizes, target_sizes) / batch_size)

            else:
                tag_score, target = self._loss_preprocess(tag_score, sample["padded_label_idx"])
                loss_total += float(loss_function(tag_score, target))  # averaged over batch index

        print("True captcha to predicted captcha: ")
        pprint(true_predicted)
        self.tensorboard_writer.add_text("eval_ture_to_predicted", str(true_predicted), global_step=step)

        accuracy = float(sequence_correct) / total
        avg_loss = float(loss_total) / (total / batch_size)
        character_accuracy = float(character_correct) / character_total
        self.tensorboard_writer.add_scalar("eval_sequence_accuracy", accuracy, global_step=step)
        self.tensorboard_writer.add_scalar("eval_character_accuracy", character_accuracy, global_step=step)
        self.tensorboard_writer.add_scalar("eval_loss", avg_loss, global_step=step)
        self.zero_grad()
        self.train()


    def _loss_preprocess(self, tag_score, target):
        '''
        :param tag_score:  value return by self.forward
        :param target:     sample["padded_label_idx"]
        :return:           (processed_tag_score, processed_target)  ready for NLLoss function
        '''
        target = target.transpose(0, 1)
        target = target.contiguous()
        target = target.view(target.size(0) * target.size(1))
        tag_score = tag_score.view(-1, self.chracter_space_length)

        return tag_score, target


    def _loss_preprocess_ctc(self, tag_score, sample):
        target_2d = [
            [int(ele) for ele in sample["padded_label_idx"][row, :] if int(ele) != 0 and int(ele) != 1]
            for row in range(sample["padded_label_idx"].size(0))]
        target = []
        for ele in target_2d:
            target.extend(ele)
        target = autograd.Variable(torch.IntTensor(target))

        # tag_score = F.softmax(F.sigmoid(tag_score), dim=-1)
        tag_score_sizes = autograd.Variable(torch.IntTensor([self._max_captcha_length] * tag_score.size(1)))
        target_sizes = autograd.Variable(sample["captcha_length"].int())

        return tag_score, target, tag_score_sizes, target_sizes


    # def visualize_graph(self, dataset):
    #     '''Since pytorch use dynamic graph, an input is required to visualize graph in tensorboard'''
    #     # warning: Do not run this, the graph is too large to visualize...
    #     sample = dataset[0]
    #     input_image = autograd.Variable(sample["image"].view(1, 3, 255, 255))
    #     tag_score = self.forward(input_image)
    #     self.tensorboard_writer.add_graph(self, tag_score)


    def save_model(self, model_path):
        self.tensorboard_writer.close()
        self.tensorboard_writer = None          # can't be pickled
        torch.save(self, model_path)
        self.tensorboard_writer = SummaryWriter(TENSORBOARD_LOG_PATH)


    @classmethod
    def load_model(cls, model_path=MODEL_PATH, *args, **kwargs):
        net = cls(*args, **kwargs)
        if os.path.exists(model_path):
            model = torch.load(model_path)
            if model:
                model.tensorboard_writer = SummaryWriter(TENSORBOARD_LOG_PATH)
                net = model

        return net


    def __del__(self):
        if self.tensorboard_writer:
            self.tensorboard_writer.close()


if __name__ == "__main__":
    from dataset_util.dataset import dataset, eval_dataset
    data_loader = DataLoader(dataset, batch_size=2, shuffle=True)
    eval_data_loader = DataLoader(eval_dataset, batch_size=2, shuffle=True)

    net = CNN_RNN.load_model()

    net.train_net(data_loader, eval_data_loader=eval_data_loader)
    # net.predict(dataset[0]["image"].view(1, 3, 255, 255))

    # predict_pil_image test code
    # from config.config import IMAGE_PATHS
    # import glob
    # from PIL import Image
    #
    # image_paths = glob.glob(os.path.join(IMAGE_PATHS.get("EVAL"), "*.png"))
    # for image_path in image_paths:
    #     pil_image = Image.open(image_path)
    #     predicted, score = net.predict_pil_image(pil_image)
    #     print("True value: {}, predicted: {}".format(os.path.split(image_path)[1], predicted))

    print("Done")

以上代码是主要部分。如果您需要使其运行的其他组件,请发表评论。卡在这里很久了。任何有关培训 crnn + ctc 的建议都值得赞赏。

【问题讨论】:

  • 你能给出一些样本,当你输入图像时,网络输出什么?给定一张图片,它会输出 (1) 废话还是 (2) 空字符串?此外,您能否总结一下您的代码(它很长),例如您使用哪种优化器?
  • 我没有让 Beam Search 解码器工作。根据每个序列步骤中概率最高的字符,模型首先预测空字符串,然后是 1 个字符,2 个字符。但是在整个图像中,它预测相同的字符。我正在使用带有 pytorch 默认参数的 Adam。我注意到一个有趣的事情是,训练损失方差在训练过程中变得越来越大,直到评估损失停止减少。上面的代码将图像提供给 resnet 预训练的 CNN 层,然后是双向 LSTM、全连接层,最后是 CTC 损失。
  • 始终输入相同的图像,并检查在这种简单情况下损失是否为零。尝试其他学习率并检查这是否有所作为。对于解码,您可以使用简单快速的最佳路径解码:获取每个时间步最可能的字符、删除重复字符、删除空白。在调试此类系统时,查看解码输出真的很有帮助。
  • 1 张图片,3 张图片,5 张图片都可以。同样的大损失方差也会发生。可能是优化器和超参数的问题。当损失停止减少时,我将尝试不同的优化器和超参数。谢谢,如果有任何新情况发生,我会通知您。
  • 还是不行。我将尝试 patapouf_ai 的以下评论。还是谢谢。

标签: python neural-network computer-vision deep-learning pytorch


【解决方案1】:

您有几个问题,所以我会尝试一一回答。

首先,为什么在验证码中添加空格会破坏模型?

神经网络学会处理它所训练的数据。如果您更改数据的分布(例如通过在字符之间添加空格),则无法保证网络会泛化。正如您在问题中暗示的那样。您训练的验证码可能总是使字符处于相同的位置,或者彼此之间的距离相同,因此您的模型会学习这一点,并通过查看这些位置来学习利用这一点。如果您希望您的网络概括特定场景,您应该明确地在该场景上进行训练。所以在你的情况下,你也应该在训练期间添加随机空间。

二、为什么loss不低于16?

很明显,从您的训练损失也停滞在 16(就像您的验证损失)这一事实来看,问题在于您的模型根本没有能力处理问题的复杂性。换句话说,您的模型拟合不足。你有正确的反应来尝试增加你的网络容量。您试图增加 LSTM 的容量,但没有帮助。因此,下一个合乎逻辑的步骤是网络的卷积部分不够强大。所以这里有一些你可能想尝试的事情,从我认为最有可能成功到最不可能成功:

  1. 使 convnet 可训练:我注意到您使用的是预训练的 convnet,并且您没有微调该 convnet 的权重。那可能是个问题。无论您的 convnet 接受过什么培训,它都可能无法开发出处理验证码所需的功能。您也应该尝试学习卷积网络的权重,以便为验证码开发有用的功能。

  2. 使用更深的 convnet: 这是幼稚的做法。您的 convnet 没有足够好的功能,请尝试更强大的更深的功能。 (但您绝对应该在使 convnet 可训练之后才使用它)。

【讨论】:

  • 谢谢!一旦我意识到空间问题,我就一直在尝试在新数据集上训练我的模型,并随机添加空间。我已经阅读了有关使用 LSTM 和 CTC 的类似层和单元进行此验证码识别的报告。所以我并没有过多考虑它的容量。 CNN 容量可能是个问题。我会尝试训练不同层数的 CNN。
  • 确保你也训练你的 CNN。将param.requires_grad = True 添加到您的 CNN 的更多层中。
  • 尝试让 CNN 可训练,但没有真正改变。我认为这是由 CTC 引起的问题。参考:github issue。似乎有不少人陷入这种情况。
【解决方案2】:

根据我的经验,训练带有 CTC 损失的 RNN 模型并非易事。如果训练没有仔细设置,模型可能根本不会收敛。以下是我的建议:

  1. 在训练过程中检查 CTC 损失输出。对于会收敛的模型,每批的 CTC 损失波动显着。如果您观察到 CTC 损失几乎单调收缩到一个稳定值,那么模型很可能会停留在局部最小值
  2. 使用短样本预训练您的模型。尽管我们有像 LSTMGRU 这样的高级 RNN 结构,但仍然很难反向传播 RNN 长步骤。
  3. 扩大样本种类。您甚至可以添加人工样本来帮助您的模型逃离局部最小值。

仅供参考,我们刚刚开源了一个新的深度学习框架Dandelion,它具有内置的 CTC 目标,并且界面非常类似于 pytorch。您可以使用 Dandelion 试用您的模型,并将其与您当前的实现进行比较。

【讨论】:

    【解决方案3】:

    我一直在训练 ctc loss 并遇到了同样的问题。我知道这是一个相当晚的答案,但希望它会帮助其他正在研究这个问题的人。经过反复试验和大量研究,在使用 ctc 进行训练时,有几点值得了解(如果您的模型设置正确):

    1. 模型降低成本的最快方法是仅预测空白。这在一些论文和博客中有所说明:请参阅http://www.tbluche.com/ctc_and_blank.html
    2. 该模型首先学会仅预测空白,然后开始接收与正确基础标签有关的错误信号。这也在上面的链接中进行了解释。在实践中,我注意到我的模型在几百个 epoch 后开始学习真正的底层标签/目标,并且损失再次开始急剧下降。与此处显示的玩具示例类似:https://thomasmesnard.github.io/files/CTC_Poster_Mesnard_Auvolat.pdf
    3. 这些参数对您的模型是否收敛有很大影响 - 学习率、批量大小和 epoch 数。

    【讨论】:

    • 感谢这有很大帮助!感谢您的回复。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-01-02
    • 2018-01-16
    • 2020-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多