-
车载人工智能(AI in automobile)在汽车领域的应用前景广泛,主要包括自动驾驶、智能驾驶辅助、智能车辆管理、智能安全系统等方面。下面是车载AI的具体应用和技术细节:自动驾驶:自动驾驶是AI在汽车领域最引人注目的应用之一。它通过各种传感器,如激光雷达、摄像头、毫米波雷达、GPS、惯性导航等,实现对周围环境的感知,通过计算机视觉和深度学习等技术进行图像处理和决策,实现车辆的自主导航和驾驶。目前,自动驾驶技术已经得到了广泛应用,例如谷歌的Waymo自动驾驶汽车在美国亚利桑那州试运行,特斯拉在其汽车中引入了“自动驾驶”功能,可以实现自主驾驶和自动泊车等功能。智能驾驶辅助:AI还可以用于提高驾驶员的驾驶安全性和舒适性。例如,通过深度学习技术,汽车可以识别驾驶员的行为,预测驾驶员的意图,并采取相应措施防止事故发生。此外,AI还可以用于车辆能效管理,通过智能导航和路线规划,帮助车主节省燃油费用并降低排放。智能车辆管理:AI可以用于车辆管理和维护,例如预测车辆维护和更换部件的时间,以减少故障和提高车辆的寿命。此外,AI还可以用于车辆远程控制和自动化,例如通过智能手机应用程序实现远程启动、锁车、调节温度等。智能安全系统:AI可以用于车载安全系统,例如通过面部识别技术实现驾驶员身份验证,以确保车辆的安全使用。此外,AI还可以用于车辆碰撞检测和预警系统,以减少事故发生的可能性。总之,AI在车载领域的应用前景非常广阔,可以提高驾驶安全性和驾驶体验,并帮助减少交通拥堵和环境污染。未来,随着技术的不断提升和应用场景的不断扩大,AI在汽车领域的应用将会更加广泛和深入。
-
在人工智能方面的对话逻辑,通常指的是让计算机能够理解和生成自然语言的算法和模型。这些算法和模型可以用于实现各种自然语言处理任务,例如问答系统、聊天机器人、语言翻译等。在对话逻辑中,最核心的部分是自然语言处理(NLP,Natural Language Processing)。NLP主要涉及词法分析、句法分析和语义分析等任务。在词法分析阶段,对文本进行分词,将连续的文本转化为单词序列;在句法分析阶段,对句子进行语法分析,确定单词之间的结构关系;在语义分析阶段,对句子进行语义理解,确定单词之间的意义关系。目前,最先进的语言模型是基于神经网络的深度学习模型,例如GPT-3和BERT等。GPT-3是一种代表现在最先进的语言模型,它使用深度学习技术来处理自然语言,可以提取自然语言中的上下文信息,并能够以人类类似的方式生成文本响应。除了NLP之外,对话逻辑还包括其他辅助技术,例如分布式语义、框架式语义和模型式语义等。分布式语义可以通过词向量空间模型来表达词之间的相似性;框架式语义可以通过框架结构来表达句子的语义信息;模型式语义可以通过模型结构来表达句子的意义。在实际应用中,可以根据不同的需求选择不同的对话逻辑技术来实现不同的自然语言处理任务。例如,可以使用问答系统来实现用户提问和系统回答的交互方式;可以使用聊天机器人来实现用户和机器人之间的自由对话;可以使用语言翻译来实现不同语言之间的翻译转换等。
-
自动语音识别(ASR,Automatic Speech Recognition)是一种语音识别技术,其目标是通过对人类语音信号的转换,将其中包含的语音内容转换为计算机可读的输入,例如按键、二进制编码或者字符序列。ASR的实现需要经过以下主要步骤:预处理(Pre-processing):在此阶段,ASR系统会对输入的原始语音信号进行一系列处理,包括噪声抑制、混响消除、分帧和归一化等操作,以提取出有效的语音特征。特征提取(Feature Extraction):在预处理之后,ASR系统会进一步提取语音特征,将原始的语音信号转化为一个更抽象、更易于处理的形式。例如,梅尔频率倒谱系数(MFCC)和感知线性预测系数(PLP)等声学特征就是常见的选择。声学模型训练(Acoustic Model Training):声学模型用于将声学特征转换为音素或单词。这个模型通常由基于深度学习的神经网络实现,其中最常见的类型是循环神经网络(RNN)和长短时记忆网络(LSTM)。语言模型训练(Language Model Training):语言模型用于估计某个词出现的概率。语言模型通常也由基于深度学习的神经网络实现,但它们更多关注的是语言的结构和语法,而非具体的声学特征。解码和识别(Decoding and Recognition):最后,ASR系统会根据声学模型和语言模型,对输入的语音信号进行解码和识别,输出最可能的文本内容。ASR技术广泛应用于各种场景,如智能客服、智能家居、车载信息娱乐系统等。随着技术的不断发展,ASR的准确性和可靠性也在不断提高,为智能语音交互提供了更多的可能性和便利。
-
语音识别全链路(Speech Recognition End-to-End)是一种将人类语音中的词汇内容转换为计算机可读的输入的技术。它主要包括语音的预处理、特征提取、声学模型和语言模型的训练以及解码和识别等步骤。语音合成和转写技术的工作原理主要是通过计算机对人类语言进行处理。其中,文本到语音(TTS)系统可以将输入的文本转换为语音信号,而语音到文本(TA)系统则可以将人类说出的语音转换为文本。这两个系统相互协作,可以将人类的语言转化为可机器理解的数字信号,从而进一步进行语音合成和转写。具体来说,TTS系统首先将输入的文本转换为数字信号,然后使用特定的算法将这些信号转化为声音。这个过程中,文本被编码为数字信号,并通过一系列的声音转换技术,最终生成高质量的语音输出。TA系统则是将人类说出的语音转换为文本。这个过程需要先对语音信号进行预处理,例如去除噪音和进行特征提取。然后,系统会利用训练好的声学模型来预测人类说话的声音特征,并将这些特征转化为对应的文本。大规模的语音合成和转写系统通常需要进行标准化和可复制性的设计,以确保系统的可靠性和一致性。这种标准化和可复制性在语音处理领域非常重要,因为不同的系统和算法可能会产生不同的结果,这可能会影响语音合成的质量和可读性。因此,为了实现高质量的语音合成和转写,需要不断优化和改进这两个系统,并确保它们能够协同工作。以我们日常询问的逻辑来说:如下图所示语音交互全链条包括四个主要环节:语音识别、语音合成、动作执行和回复生成。这些环节相互衔接,使得机器能够理解人类的语音,并给出相应的回应。首先,语音识别是将人类语音转换为机器可读的数字信号。在这个环节,机器会对收集到的语音进行预处理,包括去除噪音、增强语音信号等。然后,通过对语音信号的分析,将语音转换为文本,这是实现语音交互的基础。接下来是语音合成环节。语音合成是将文本转化为语音的过程,它利用自然语言处理技术来解析和理解语音,并提取关键信息。在这个环节,机器会将输入的文本转换为可听的声音,使人机之间能够进行更加自然的交流。动作执行是将获取到的信息转换为指令或操作。例如,当用户通过语音指令要求系统发送一条短信时,系统会将该指令转换为具体的短信发送操作。动作执行还包括对语音交互过程中的各种操作进行管理和调度,例如对多个语音指令进行优先级排序、协调多个操作之间的顺序等。回复生成则是根据指令或操作的结果返回相应的响应或反馈。例如,当用户要求发送短信给某人时,系统会根据用户的请求生成一条包含接收人和短信内容的短信,并发送给指定的接收人。回复生成还包括对回复的格式、语言风格等进行管理和控制,以确保回复的质量和准确性。最后,对话管理是协调整个流程中的沟通和管理,确保用户能够顺畅地与系统互动。它通过对语音交互过程的监控和管理,确保各个环节之间的顺畅衔接和协调合作,从而提高整个语音交互的效率和准确性。综上所述,语音交互全链条是实现语音交互技术的关键环节,它包括了语音识别、语音合成、动作执行和回复生成等环节。这些环节相互衔接、协调合作,使得机器能够理解人类的语音,并给出相应的回应。随着人工智能技术的不断发展,语音交互全链条将会继续优化和改进,为人类带来更加智能、便捷的交互体验。
-
自然语言生成(NLG,Natural Language Generation)是自然语言处理领域中的一个重要任务,旨在将非语言格式的数据转化为自然语言文本,以供人类阅读和理解。NLG的目标是生成语法通顺、语义准确的自然语言文本,例如文章、报告、对话等。一、NLG的基本任务文本规划:根据给定的结构化数据或文本素材,确定要表达的主题、内容和结构。语句规划:将结构化数据或文本素材转化为一系列的语句,保持语法和语义的准确性。实现:将语句组合成语法通顺、语义准确的自然语言文本。二、NLG的技术和方法模板生成:根据给定的结构化数据或文本素材,自动生成模板化的文本内容。文本到文本:将非语言格式的数据转化为自然语言文本,例如机器翻译、文本摘要等任务。数据到文本:将结构化数据或文本素材转化为自然语言文本,例如智能报告、对话系统等任务。上下文感知:根据前文的内容和语境,生成与上下文相符合的自然语言文本。情感分析:根据给定的文本素材,生成带有情感色彩的自然语言文本。三、NLG的应用智能报告:根据结构化数据自动生成报告、新闻报道等文本。对话系统:根据用户的输入和对话历史,生成回复、问题回答等文本。机器翻译:将非语言格式的数据转化为另一种语言的文本。自动写作:根据给定的主题和素材,自动生成文章、故事等文本。四、总结NLG是自然语言处理领域中的一个重要任务,它的目标是将非语言格式的数据转化为自然语言文本。NLG技术可以应用于智能报告、对话系统、机器翻译、自动写作等多个领域中。随着技术的不断发展,NLG将会在更多的场景中发挥重要作用。然而,面临的挑战和问题也不容忽视,如数据隐私、算法透明度、伦理问题等。因此,我们需要在推动NLG技术发展的同时,关注这些问题的解决,以实现人工智能技术的可持续发展。
-
自然语言理解(NLU,Natural Language Understanding)是人工智能领域中自然语言处理的一个子任务,旨在让计算机能够理解和解释人类语言。NLU的目标是让计算机可以从自然语言文本中提取出意义、语义和意图,从而进行各种自然语言处理任务,例如文本分类、信息抽取、问答等。一、NLU的基本任务词义消歧:给定一个词,确定其在特定语境中的含义。词性标注:将句子中的每个词标注其对应的词性,例如动词、名词、形容词等。句法分析:分析句子的结构和语法关系,确定句子中的主语、谓语、宾语等成分。语义理解:理解句子的意义和意图,例如进行问答、文本分类、情感分析等任务。信息抽取:从文本中提取出关键信息,例如时间、地点、人物、事件等。机器翻译:将一种语言翻译成另一种语言,保持原文的意义和风格。二、NLU的技术和方法词嵌入:将词汇映射到高维空间中的技术,可以将词汇转化为计算机可计算的形式,以便进行向量计算和机器学习任务。深度学习:通过多层的神经网络来模拟自然语言的复杂结构,可以理解和提取自然语言的语义和意图。规则和统计方法:可以使用规则和统计方法来进行词性标注、句法分析、文本分类等任务。预训练模型:利用大量语料库进行预训练,从而让模型可以理解和处理各种自然语言任务。三、NLU的应用智能问答:根据用户的问题,利用NLU技术理解问题的含义,然后从知识库或互联网中查找并返回答案。机器翻译:利用NLU技术将一种语言翻译成另一种语言,保持原文的意义和风格。情感分析:利用NLU技术分析文本的情感倾向,例如判断一条评论是正面还是负面情感。信息提取:利用NLU技术从文本中提取出关键信息,例如时间、地点、人物、事件等。自动写作:利用NLU技术生成自然语言的文本,例如新闻报道、故事、对话等。四、总结NLU是人工智能领域中非常重要的一个子任务,它可以实现各种自然语言处理任务,例如文本分类、信息抽取、问答等。随着技术的不断发展,NLU将会在更多的场景中发挥重要作用。然而,面临的挑战和问题也不容忽视,如数据隐私、算法透明度、伦理问题等。因此,我们需要在推动NLU技术发展的同时,关注这些问题的解决,以实现人工智能技术的可持续发展。重新生成
-
随着人工智能技术的不断发展,自然语言处理(NLP)已经成为了人机交互、信息检索、智能客服等领域的关键技术。本文将介绍人工智能NLP的基本概念、技术原理以及在商业和个人应用场景中的潜在价值。一、基本概念NLP是一种人工智能技术,它能够模拟人类对自然语言的理解和生成。NLP的目标是将自然语言文本转化为计算机可理解的格式,从而进行信息提取、文本分类、机器翻译等任务。二、技术原理神经网络:NLP的核心技术之一是神经网络,它是一种模拟人脑神经元结构的数据处理模型。通过训练,神经网络可以学习到自然语言的语法、语义和语用规律。深度学习:深度学习是人工智能领域的一种重要技术,它通过构建多层神经网络来模拟人类的学习过程。在NLP中,深度学习可以帮助机器更好地理解自然语言的复杂结构。反向传播算法:反向传播算法是一种训练神经网络的重要算法,它通过计算输出与期望结果的误差来调整网络参数,使得下一次输出更接近期望结果。语言模型:语言模型是根据大量文本数据统计出的语言规律,用于对自然语言文本进行分类、预测和生成等任务。序列标注:序列标注是一种对序列数据进行分类和标注的方法,例如分词、词性标注、命名实体识别等任务。三、应用场景商业领域:智能客服:利用NLP技术理解用户问题并给出准确答案,提高客户满意度。广告推荐:通过分析用户历史行为和查询词,利用NLP技术为用户提供精准广告推荐。舆情监测:利用NLP技术对大量文本数据进行情感分析,帮助企业及时掌握市场动态和消费者情绪。个人用户领域:智能助手:利用NLP技术实现语音识别和智能问答,帮助用户完成日常任务。个人助手:通过NLP技术分析用户的个人数据和行为,提供个性化的建议和服务。四、结论人工智能NLP技术在商业和个人应用领域都具有广泛的应用前景。随着技术的不断发展,NLP将会在更多的场景中发挥重要作用。然而,面临的挑战和问题也不容忽视,如数据隐私、算法透明度、伦理问题等。因此,我们需要在推动NLP技术发展的同时,关注这些问题的解决,以实现人工智能NLP技术的可持续发展。参考文献:Goldberg, Y., & Levy, O. (2016). Neural network methods for natural language processing. Cambridge University Press.Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119).Turian, J., Ratinov, L., & Bengio, Y. (2010). Word representations: A simple and general method for semi-supervised learning. In Proceedings of the 48th annual meeting of the Association for Computational Linguistics (pp. 384-394).
-
它如何使用 argmax() 函数?在对每个类应用上述数学函数后,Softmax 会为每个类计算一个介于 0 和 1 之间的值。现在我们每个类都有几个值,为了分类输入属于哪个类,Softmax 使用 argmax() 给出了应用 Softmax 后具有最大值的值的索引。为什么 Softmax 只用在神经网络的最后一层?现在进入重要部分,Softmax 仅用于最后一层以对值进行归一化,而其他激活函数(relu、leaky relu、sigmoid 和其他各种)用于内层。如果我们看到其他激活函数,如 relu、leaky relu 和 sigmoid,它们都使用唯一的单个值来带来非线性。他们看不到其他值是什么。但是在 Softmax 函数中,在分母中,它取所有指数值的总和来归一化所有类的值。它考虑了范围内所有类的值,这就是我们在最后一层使用它的原因。要通过分析所有的值来知道Input属于哪个类。对 Softmax 的误解关于 Softmax 的第一个也是最大的误解是,它通过归一化值的输出是每个类的概率值,这完全错误。这种误解是因为这些值的总和为 1,但它们只是归一化值而不是类的概率。在最后一层并不是单独使用 Sotmax,我们更喜欢使用 Log Softmax,它只是对来自 Softmax 函数的归一化值进行对数。Log Softmax 在数值稳定性、更便宜的模型训练成本和 Penalizes Large error(误差越大惩罚越大)方面优于 Softmax。这就是在神经网络中用作激活函数的 Softmax 函数。相信读完本文后你对它已经有了一个清楚的了解。
-
Softmax是个大家都熟悉的激活函数,然而,很多人只知道它的表达式,它在网络中的位置,而对一些具体的原因和细节却回答不上来。这篇文章给了相应的介绍。 Softmax 是一个数学函数,用于对 0 和 1 之间的值进行归一化。在本文中,您将了解:什么是 Softmax 激活函数及其数学表达式?它是如何使用 argmax() 函数实现的?为什么 Softmax 只用在神经网络的最后一层?对 Softmax 的误解什么是 Softmax 激活函数及其数学表达式?在深度学习的时候,使用 Softmax 作为激活函数,对 0 到 1 之间的向量中每个值的输出和尺度进行归一化。Softmax 用于分类任务。在网络的最后一层,会生成一个 N 维向量,分类任务中的每个类对应一个向量。网络输出层中的 N 维向量Softmax 用于对 0 和 1 之间的那些加权和值进行归一化,并且它们的和等于 1,这就是为什么大多数人认为这些值是类的概率,但这是一种误解,我们将在本文中讨论它。实现 Softmax 函数的公式:使用这个数学表达式,我们计算每类数据的归一化值。这里 θ(i) 是我们从展平层得到的输入。计算每个类的归一化值,分子是类的指数值,分母是所有类的指数值之和。使用 Softmax 函数,我们得到 0 到 1 之间的所有值,所有值的总和变为等于 1。因此人们将其视为概率,这是他们的误解。
-
只训练模型的一部分参数例如,只想训练上面的model中的layer参数,而保持layer2的参数不动。可以如下设置Optimizer:model = Net() optimizer_Adam = torch.optim.Adam(model.layer.parameters(), lr=0.1) # 只传入layer层的参数,就可以只更新layer层的参数而不影响其他参数。不同部分的参数设置不同的学习率(以及其他属性)例如,要想使model的layer参数学习率为0.1,layer2的参数学习率为0.2,可以如下设置Optimizer:model = Net() params_dict = [{'params': model.layer.parameters(), 'lr': 0.1}, {'params': model.layer2.parameters(), 'lr': 0.2}] optimizer_Adam = torch.optim.Adam(params_dict)这种方法更为灵活,手动构造一个params_dict列表来初始化Optimizer。注意,字典中的参数部分的 key 必须为 ‘params’。这样就可以灵活的设置Optimizer啦。动态更新learning rate了解了Optimizer的基本结构和使用方法,接下来就可以看一下,如何在训练过程中动态更新learning rate。手动修改lr上面我们了解到,Optimizer的每一组参数维护一个lr,因此,最直接的方法就是我们在训练过程中手动修改Optimizer中对应的lr的值:model = Net() # 生成网络 optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 生成优化器 lr_list = [] for epoch in range(100): # 假设迭代100次 if epoch % 5 == 0: # 每迭代5次,更新一次学习率 for params in optimizer_Adam.param_groups: # 遍历Optimizer中的每一组参数 params['lr'] *= 0.9 # 将该组参数的学习率 * 0.9 # params['weight_decay'] = 0.5 # 当然也可以修改其他属性 lr_list.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list, color='r') plt.show()orch.optim.lr_schedulertorch.optim.lr_scheduler包中提供了一些类,用于动态修改lr。torch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_sheduler.CosineAnneaingLRtorch.optim.lr_scheduler.ReduceLROnPlateau注意: pytorch 1.1.0版本之后,在创建了lr_scheduler对象之后,会自动执行第一次lr更新(可以理解为执行一次scheduler.step())。因此,在使用的时候,需要先调用optimizer.step(),再调用scheduler.step()。如果创建了lr_scheduler对象之后,先调用scheduler.step(),再调用optimizer.step(),则会跳过了第一个学习率的值。# 调用顺序 loss.backward() optimizer.step() scheduler.step() ...注意: 创建scheduler时,所传入的Optimizer的param_groups必须有一个initial_lr键作为初始学习率。如果last_epoch=-1,则用于初始化的Optimizer可以没有initial_lr键,以 lr 键初始化为initial_lr。torch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)Optimizer:优化器实例lr_lambda:是一个函数(常用lambda表达式)或函数列表,该函数接收一个int参数(epoch),然后计算出一个系数α,最后学习率更新为。其中lr_lambda如果传入多个函数的list的话,则对应每组param_groups的学习率调整策略。last_epoch:(int)上一个epoch数。默认为-1,且当last_epoch=-1时,将lr设置为initial_lr。第一次更新lr时,就按照epoch = last_epoch + 1更新。(比如last_epoch = 1,则第一次lr更新时,就将epoch=2传入上面的lr_lambda函数,得出系数α需要注意的是,该scheduler每次lr更新,是用initial_lr 乘以系数 α,而不是用上一次的lr 乘以系数 α,即orch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)每迭代step_size次,学习率乘以gamma。model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 创建scheduler,每迭代5次,学习率衰减一半 scheduler = torch.optim.lr_scheduler.StepLR(optimizer_Adam, step_size=5, gamma=0.5, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)多段衰减法。milestones传入一个list,指定多个epoch数,每迭代到指定的epoch次数时,lr乘以gamma。例子:model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 初始 lr=0.1 # lr 变化 # 0 - 20 epoch: 0.1 # 21 - 40 epoch: 0.05 # 41 - 60 epoch: 0.025 # 60 - 80 epoch: 0.0125 # 80 - end epoch: 0.01125 scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer_Adam, milestones=[20, 40, 60, 80], gamma=0.5, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()torch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, last_epoch=-1)每个epoch按指数衰减 lr。model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 指数衰减学习率,衰减率为 gamma=0.9 scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer_Adam, gamma=0.9, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.CosineAnnealingLRtorch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0, last_epoch=-1)按照三角函数规则来更新学习率。 表示最小学习率, 即正弦函数最低点 表示最大学习率, 设置为initial_Ir (在last_epoch=-1 时, 即为Ir)。 表示当前epoch数 表示 个 周期所对应的epoch数值model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 周期为50epoch,lr最小值为0(默认) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer_Adam, eta_min=0, T_max=25, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.ReduceLROnPlateautorch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, verbose=False, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-08)根据指定的指标量来调整学习率。如果指标量停止变化时,就减小学习率。参数:mode:(str),从(min, max)中选择。min:如果指定量不再下降,就减小lrmax:如果指定量不再上升,就减小lrfactor:(float),衰减因子,每次更新lr = lr * factorpatience:(int),容忍度,如果经过patience次迭代后,指标没有变化(上升或下降),就更新lr。verbose:(bool),每次更新lr,是否向std输出。threshold:(float),阈值,对于制定的指标只有超过阈值才算有变化threshold_mode:(str),从(rel,abs)总选择。性能衡量方式。max模式下:dynamic_threshold = best + thresholdmin模式下:dynamic_threshold = best - thresholdmax模式下:dynamic_threshold = best * ( 1 + threshold )min模式下:dynamic_threshold = best * ( 1 - threshold )rel:abs:cooldown:(int),每次调整lr之后,冷却cooldown个epoch,避免lr下降过快min_lr:(float or list),学习率最小值。如果给定一个标量值,就param_groups中所有组都设置该最小值;也可以用一个list为每组指定一个最小值。eps:(float),lr变化最小值,如果lr的两次变化差距小于eps,则忽略这次变化。optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9) scheduler = ReduceLROnPlateau(optimizer, 'min') for epoch in range(10): train(...) val_loss = validate(...) # Note that step should be called after validate() scheduler.step(val_loss)
-
导读 在很多学习过程中,都会采用动态调整学习率的方法。刚开始训练的时候,学习率设置大一点,以加快学习速度;之后逐渐减小学习率,来寻找最优解。那么在Pytorch中,如在训练过程中动态地调整学习率呢? 学习率设置对于学习过程来说相当重要。学习率过低会导致学习速度太慢,学习率过高又容易导致难以收敛。在很多学习过程中,都会采用动态调整学习率的方法。刚开始训练的时候,学习率设置大一点,以加快学习速度;之后逐渐减小学习率,来寻找最优解。那么在Pytorch中,如在训练过程中动态地调整学习率呢?目录优化器Optimizer只训练模型的一部分参数不同部分的参数设置不同的学习率(以及其他属性)Optimizer基本属性optimizer基本方法动态更新learning ratetorch.optim.lr_schedulertorch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_scheduler.CosineAnnealingLRtorch.optim.lr_scheduler.ReduceLROnPlateau手动修改lr优化器Optimizer在说学习率调整方法之前,先来了解一下Pytorch中的优化器Optimizer机制。用过Pytorch的都知道,模型训练时的固定搭配。loss.backward() optimizer.step() optimizer.zero_grad() ...简单来说,loss.backward()就是反向计算出各参数的梯度,然后optimizer.step()就是更新网络中的参数,optimizer.zero_grad()将这一轮的梯度清零,防止这一轮的梯度影响下一轮的更新。常用优化器都在torch.optim包中,因此需要先导入包:import torch.optim.Adam import torch.optim.SGD这里以常用的Adam优化器和SGD优化器为例,介绍一下Pytorch中的优化器使用方法。假设我们有一个网络如下,下面的例子都以此网络作为例子:class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.layer = nn.Linear(10, 2) self.layer2 = nn.Linear(2, 10) def forward(self, input): return self.layer(input) # Optimizer基本属性所有Optimizer公有的一些基本属性:lr: learning rate,学习率eps: 学习率最小值,在动态更新学习率时,学习率最小不会小于该值。weight_decay: 权值衰减。相当于对参数进行L2正则化(使模型复杂度尽可能低,防止过拟合),该值可以理解为正则化项的系数。betas: (待研究)amsgrad: (bool)(待研究)每个Optimizer都维护一个param_groups的list。该list中维护需要优化的参数以及对应的属性设置。optimizer基本方法add_param_group(param_group): 为optimizer的param_groups增加一个参数组。这在微调预先训练的网络时非常有用,因为冻结层可以训练并随着训练的进行添加到优化器中。load_state_dict(state_dict): 加载optimizer state。参数必须是optimizer.state_dict()返回的对象。state_dict(): 返回一个dict,包含optimizer的状态:state和param_groups。step(closure): 执行一次参数更新过程。zero_grad(): 清除所有已经更新的参数的梯度。我们在构造优化器时,最简单的方法通常如下:model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1)model.parameters()返回网络model的全部参数。将model的全部参数传入Adam中构造出一个Adam优化器,并设置 learning rate=0.1。因此该 Adam 优化器的 param_groups 维护的就是模型 model 的全部参数,并且学习率为0.1。这样在调用optimizer_Adam.step()时,就会对model的全部参数进行更新。Optimizer的param_groups是一个list,其中的每个元素都是一组独立的参数,以dict的方式存储。结构如下:-param_groups -0(dict) # 第一组参数 params: # 维护要更新的参数 lr: # 该组参数的学习率 betas: eps: # 该组参数的学习率最小值 weight_decay: # 该组参数的权重衰减系数 amsgrad: -1(dict) # 第二组参数 -2(dict) # 第三组参数 ... ...这样可以实现很多灵活的操作,比如以下。
-
训练框架代码讲解训练框架相关代码实现在 ocr_by_transformer.py 文件中下面开始逐步讲解代码,主要有以下几个部分:构建dataset → 图像预处理、label处理等;模型构建 → backbone + transformer;模型训练推理 → 贪心解码下面一步步来看4.1 准备工作首先导入后面需要用到的库import os import time import copy from PIL import Image # 在线数据集相关包 from tensorbay import GAS from tensorbay.dataset import Dataset # torch相关包 import torch import torch.nn as nn from torch.autograd import Variable import torchvision.models as models import torchvision.transforms as transforms # 导入工具类包 from analysis_recognition_dataset import load_lbl2id_map, statistics_max_len_label from transformer import * from train_utils import *然后设置一些基础的参数device = torch.device('cuda') # 'cpu'或者'cuda' nrof_epochs = 1500 # 迭代次数,1500,根据需求进行修正 batch_size = 64 # 批量大小,64,根据需求进行修正 model_save_path = './log/ex1_ocr_model.pth'在线获取图像数据,并读取图像label中字符与其id的映射字典,后续Dataset创建需要使用。# GAS凭证 KEY = 'Accesskey-fd26cc098604c68a99d3bf7f87cd480a' gas = GAS(KEY) # 在线获取数据集 dataset_online = Dataset("ICDAR2015", gas) dataset_online.enable_cache('./data') # 开启本地缓存 # 获取训练集和验证集 train_data_online = dataset_online["train"] valid_data_online = dataset_online['valid'] # 读取label-id映射关系记录文件 lbl2id_map_path = os.path.join('./', 'lbl2id_map.txt') lbl2id_map, id2lbl_map = load_lbl2id_map(lbl2id_map_path) # 统计数据集中出现的所有的label中包含字符最多的有多少字符,数据集构造gt(ground truth)信息需要用到 train_max_label_len = statistics_max_len_label(train_data_online) valid_max_label_len = statistics_max_len_label(valid_data_online) # 数据集中字符数最多的一个case作为制作的gt的sequence_len sequence_len = max(train_max_label_len, valid_max_label_len) 4.2 Dataset构建下面来介绍Dataset构建相关内容,首先思考下如何进行图片预处理比较合理。图片预处理方案假设图片尺寸为 经过网络后的特征图尺寸为 基于之前对于数据集的分析,图片基本都是水平长条状的,图像内容是水平排列的字符组成的单词。那么图片空间上同一纵向切片的位置,基本只有一个字符,因此纵向分辨率不需要很大,那么取 即可;而横向的分辨率需要大一些,我们需要有不同的embedding来编码水平方向上不同字符的特征。这里,我们就用最经典的resnet18网络作为backbone,由于其下采样倍数为32,最后一层特征图channel数为512,那么:那么输入图片的宽度如何确定呢?这里给出两种方案,如下图所示:方法一:设定一个固定尺寸,将图像保持其宽高比进行resize,右侧空余区域进行padding;方法二:直接将原始图像强制resize到一个预设的固定尺寸。注:这里不妨先思考下,你觉得哪种方案比较好呢?作者选择了方法一,因为图片的宽高比和图片中单词的字符数量是大致呈正比的,如果预处理时保持住原图片的宽高比,那么特征图上每一个像素对应原图上字符区域的范围就是基本稳定的,这样或许有更好的预测效果。这里还有个细节,观察上图你会发现,每个宽:高=1:1的区域内,基本都分布着2-3个字符,因此我们实际操作时也没有严格的保持宽高比不变,而是将宽高比提升了3倍,即先将原始图片宽度拉长到原来的3倍,再保持宽高比,将高resize到32。注:这里建议再次停下来思考下,刚刚这个细节又是为什么?这样做的目的是让图片上每一个字符,都有至少一个特征图上的像素与之对应,而不是特征图宽维度上一个像素,同时编码了原图中的多个字符的信息,这样我认为会对transformer的预测带来不必要的困难(仅是个人观点,欢迎讨论)。确定了resize方案, 具体设置为多少呢?结合前面我们对数据集做分析时的两个重要指标,数据集label中最长字符数为21,最长的宽高比8.6,我们将最终的宽高比设置为 24:1,因此汇总一下各个参数的设置:相关代码实现:# ---------------- # 图片预处理 # ---------------- # load image with img_data.open() as fp: img = Image.open(fp).convert('RGB') # 对图片进行大致等比例的缩放 # 将高缩放到32,宽大致等比例缩放,但要被32整除 w, h = img.size ratio = round((w / h) * 3) # 将宽拉长3倍,然后四舍五入 if ratio == 0: ratio = 1 if ratio > self.max_ratio: ratio = self.max_ratio h_new = 32 w_new = h_new * ratio img_resize = img.resize((w_new, h_new), Image.BILINEAR) # 对图片右半边进行padding,使得宽/高比例固定=self.max_ratio img_padd = Image.new('RGB', (32*self.max_ratio, 32), (0,0,0)) img_padd.paste(img_resize, (0, 0)) 图像增广图像增广并不是重点,这里我们除了上述的resize方案外,仅对图像进行常规的随机颜色变换和归一化操作。完整代码构建Dataset的完整代码如下:class Recognition_Dataset(object): def __init__(self, segment, lbl2id_map, sequence_len, max_ratio, pad=0): self.data = segment self.lbl2id_map = lbl2id_map self.pad = pad # padding标识符的id,默认0 self.sequence_len = sequence_len # 序列长度 self.max_ratio = max_ratio * 3 # 将宽拉长3倍 # 定义随机颜色变换 self.color_trans = transforms.ColorJitter(0.1, 0.1, 0.1) # 定义 Normalize self.trans_Normalize = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]), ]) def __getitem__(self, index): """ 获取对应index的图像和ground truth label,并视情况进行数据增强 """ img_data = self.data[index] lbl_str = img_data.label.classification.category # 图像标签 # ---------------- # 图片预处理 # ---------------- # load image with img_data.open() as fp: img = Image.open(fp).convert('RGB') # 对图片进行大致等比例的缩放 # 将高缩放到32,宽大致等比例缩放,但要被32整除 w, h = img.size ratio = round((w / h) * 3) # 将宽拉长3倍,然后四舍五入 if ratio == 0: ratio = 1 if ratio > self.max_ratio: ratio = self.max_ratio h_new = 32 w_new = h_new * ratio img_resize = img.resize((w_new, h_new), Image.BILINEAR) # 对图片右半边进行padding,使得宽/高比例固定=self.max_ratio img_padd = Image.new('RGB', (32*self.max_ratio, 32), (0,0,0)) img_padd.paste(img_resize, (0, 0)) # 随机颜色变换 img_input = self.color_trans(img_padd) # Normalize img_input = self.trans_Normalize(img_input) # ---------------- # label处理 # ---------------- # 构造encoder的mask encode_mask = [1] * ratio + [0] * (self.max_ratio - ratio) encode_mask = torch.tensor(encode_mask) encode_mask = (encode_mask != 0).unsqueeze(0) # 构造ground truth label gt = [] gt.append(1) # 先添加句子起始符 for lbl in lbl_str: gt.append(self.lbl2id_map[lbl]) gt.append(2) for i in range(len(lbl_str), self.sequence_len): # 除去起始符终止符,lbl长度为sequence_len,剩下的padding gt.append(0) # 截断为预设的最大序列长度 gt = gt[:self.sequence_len] # decoder的输入 decode_in = gt[:-1] decode_in = torch.tensor(decode_in) # decoder的输出 decode_out = gt[1:] decode_out = torch.tensor(decode_out) # decoder的mask decode_mask = self.make_std_mask(decode_in, self.pad) # 有效tokens数 ntokens = (decode_out != self.pad).data.sum() return img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens @staticmethod def make_std_mask(tgt, pad): """ Create a mask to hide padding and future words. padd 和 future words 均在mask中用0表示 """ tgt_mask = (tgt != pad) tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data)) tgt_mask = tgt_mask.squeeze(0) # subsequent返回值的shape是(1, N, N) return tgt_mask def __len__(self): return len(self.data)上面的代码中还涉及到几个和label处理相关的细节,属于Transformer训练相关的逻辑,这里再简单提一下:encode_mask由于我们对图像进行了尺寸调整,并根据需求对图像进行了padding,而padding的位置是没有包含有效信息的,为此需要根据padding比例构造相应encode_mask,让transformer在计算时忽略这部分无意义的区域。label处理本实验使用的预测标签与机器翻译模型训练时的标签基本一致,因此在处理方式中差异较小。标签处理中,将label中字符转换成其对应id,并在句子开始添加起始符,句子最后添加终止符,并在不满足sequence_len长度时在剩余位置进行padding(0补位)。decode_mask一般的在decoder中我们会根据label的sequence_len生成一个上三角阵形式的mask,mask的每一行便可以控制当前time_step时,只允许decoder获取当前步时之前的字符信息,而禁止获取未来时刻的字符信息,这防止了模型训练时的作弊行为。decode_mask经过一个特殊的函数 make_std_mask() 进行生成。同时,decoder的label制作同样要考虑上对padding的部分进行mask,所以decode_mask在label被padding对应的位置处也应该进行写成False。生成的decode_mask如下图所示:以上是构建Dataset的所有细节,进而我们可以构建出DataLoader供训练使用# 构造 dataloader max_ratio = 8 # 图片预处理时 宽/高的最大值,不超过就保比例resize,超过会强行压缩 train_dataset = Recognition_Dataset(train_data_online, lbl2id_map, sequence_len, max_ratio, pad=0) valid_dataset = Recognition_Dataset(valid_data_online, lbl2id_map, sequence_len, max_ratio, pad=0) # loader size info: # --> img_input: [batch_size, c, h, w] --> [64, 3, 32, 32*8*3] # --> encode_mask: [batch_size, h/32, w/32] --> [64, 1, 24] 本文backbone采用的32倍下采样,所以除以32 # --> decode_in: [bs, sequence_len-1] --> [64, 20] # --> decode_out: [bs, sequence_len-1] --> [64, 20] # --> decode_mask: [bs, sequence_len-1, sequence_len-1] --> [64, 20, 20] # --> ntokens: [bs] --> [64] train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) valid_loader = torch.utils.data.DataLoader(valid_dataset, batch_size=batch_size, shuffle=False, num_workers=4)4.3 模型构建代码通过 make_ocr_model 和 OCR_EncoderDecoder 类完成模型结构搭建。可以从 make_ocr_model 这个函数看起,该函数首先调用了pytorch中预训练的Resnet-18作为backbone以提取图像特征,此处也可以根据自己需要调整为其他的网络,但需要重点关注的是网络的下采样倍数,以及最后一层特征图的channel_num,相关模块的参数需要同步调整。之后调用了 OCR_EncoderDecoder 类完成transformer的搭建。最后对模型参数进行初始化。在 OCR_EncoderDecoder 类中,该类相当于是一个transformer各基础组件的拼装线,包括 encoder 和 decoder 等,其初始参数是已存在的基本组件,其基本组件代码都在transformer.py文件中,本文将不在过多叙述。这里再来回顾一下,图片经过backbone后,如何构造为Transformer的输入:图片经过backbone后将输出一个维度为 [batch_size, 512, 1, 24] 的特征图,在不关注batch_size的前提下,每一张图像都会得到如下所示具有512个通道的1×24的特征图,如图中红色框标注所示,将不同通道相同位置的特征值拼接组成一个新的向量,并作为一个时间步的输入,此时变构造出了维度为 [batch_size, 24, 512] 的输入,满足Transformer的输入要求。下面来看下完整的构造模型部分的代码:# 模型结构 class OCR_EncoderDecoder(nn.Module): """ A standard Encoder-Decoder architecture. Base for this and many other models. """ def __init__(self, encoder, decoder, src_embed, src_position, tgt_embed, generator): super(OCR_EncoderDecoder, self).__init__() self.encoder = encoder self.decoder = decoder self.src_embed = src_embed # input embedding module self.src_position = src_position self.tgt_embed = tgt_embed # ouput embedding module self.generator = generator # output generation module def forward(self, src, tgt, src_mask, tgt_mask): "Take in and process masked src and target sequences." # src --> [bs, 3, 32, 768] [bs, c, h, w] # src_mask --> [bs, 1, 24] [bs, h/32, w/32] memory = self.encode(src, src_mask) # memory --> [bs, 24, 512] # tgt --> decode_in [bs, 20] [bs, sequence_len-1] # tgt_mask --> decode_mask [bs, 20] [bs, sequence_len-1] res = self.decode(memory, src_mask, tgt, tgt_mask) # [bs, 20, 512] return res def encode(self, src, src_mask): # feature extract # src --> [bs, 3, 32, 768] src_embedds = self.src_embed(src) # 此处使用的resnet18作为backbone 输出-->[batchsize, c, h, w] --> [bs, 512, 1, 24] # 将src_embedds由shape(bs, model_dim, 1, max_ratio) 处理为transformer期望的输入shape(bs, 时间步, model_dim) # [bs, 512, 1, 24] --> [bs, 24, 512] src_embedds = src_embedds.squeeze(-2) src_embedds = src_embedds.permute(0, 2, 1) # position encode src_embedds = self.src_position(src_embedds) # [bs, 24, 512] return self.encoder(src_embedds, src_mask) # [bs, 24, 512] def decode(self, memory, src_mask, tgt, tgt_mask): target_embedds = self.tgt_embed(tgt) # [bs, 20, 512] return self.decoder(target_embedds, memory, src_mask, tgt_mask) def make_ocr_model(tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1): """ 构建模型 params: tgt_vocab: 输出的词典大小 N: 编码器和解码器堆叠基础模块的个数 d_model: 模型中embedding的size,默认512 d_ff: FeedForward Layer层中embedding的size,默认2048 h: MultiHeadAttention中多头的个数,必须被d_model整除 dropout: dropout的比率 """ c = copy.deepcopy # torch中预训练的resnet18作为特征提取网络, backbone backbone = models.resnet18(pretrained=True) backbone = nn.Sequential(*list(backbone.children())[:-2]) # 去掉最后两个层 (global average pooling and fc layer) attn = MultiHeadedAttention(h, d_model) ff = PositionwiseFeedForward(d_model, d_ff, dropout) position = PositionalEncoding(d_model, dropout) # 构建模型 model = OCR_EncoderDecoder( Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), backbone, c(position), nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), Generator(d_model, tgt_vocab)) # 此处的generator并没有在类内调用 # Initialize parameters with Glorot / fan_avg. for child in model.children(): if child is backbone: # 将backbone的权重设为不计算梯度 for param in child.parameters(): param.requires_grad = False # 预训练好的backbone不进行随机初始化,其余模块进行随机初始化 continue for p in child.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) return model通过上述的两个类,可以方便构建transformer模型:# build model # use transformer as ocr recognize model # 此处构建的ocr_model不含有Generator tgt_vocab = len(lbl2id_map.keys()) d_model = 512 ocr_model = make_ocr_model(tgt_vocab, N=5, d_model=d_model, d_ff=2048, h=8, dropout=0.1) ocr_model.to(device)4.4 模型训练模型训练之前,还需要定义模型评判准则、迭代优化器等。本实验在训练时,使用了标签平滑(label smoothing)、网络训练热身(warmup)等策略,以上策略的调用函数均在train_utils.py文件中,此处不涉及以上两种方法的原理及代码实现。label smoothing可以将原始的硬标签转化为软标签,从而增加模型的容错率,提升模型泛化能力。代码中 LabelSmoothing() 函数实现了label smoothing,同时内部使用了相对熵函数计算了预测值与真实值之间的损失。warmup策略能够有效控制模型训练过程中的优化器学习率,自动化的实现模型学习率由小增大再逐渐下降的控制,帮助模型在训练时更加稳定,实现损失的快速收敛。代码中 NoamOpt() 函数实现了warmup控制,采用的Adam优化器,实现学习率随迭代次数的自动调整。# train prepare criterion = LabelSmoothing(size=tgt_vocab, padding_idx=0, smoothing=0.0) # label smoothing optimizer = torch.optim.Adam(ocr_model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9) model_opt = NoamOpt(d_model, 1, 400, optimizer) # warmup模型训练过程的代码如下所示,每训练10个epoch便进行一次验证,单个epoch的计算过程封装在 run_epoch() 函数中。# train & valid ... for epoch in range(nrof_epochs): print(f"\nepoch {epoch}") print("train...") # 训练 ocr_model.train() loss_compute = SimpleLossCompute(ocr_model.generator, criterion, model_opt) train_mean_loss = run_epoch(train_loader, ocr_model, loss_compute, device) if epoch % 10 == 0: print("valid...") # 验证 ocr_model.eval() valid_loss_compute = SimpleLossCompute(ocr_model.generator, criterion, None) valid_mean_loss = run_epoch(valid_loader, ocr_model, valid_loss_compute, device) print(f"valid loss: {valid_mean_loss}") # save model torch.save(ocr_model.state_dict(), './trained_model/ocr_model.pt')SimpleLossCompute() 类实现了transformer输出结果的loss计算。在使用该类直接计算时,类需要接收(x, y, norm)三个参数,x为decoder输出的结果,y为标签数据,norm为loss的归一化系数,用batch中所有有效token数即可。由此可见,此处才正完成transformer所有网络的构建,实现数据计算流的流通。run_epoch() 函数内部完成了一个epoch训练的所有工作,包括数据加载、模型推理、损失计算与方向传播,同时将训练过程信息进行打印。def run_epoch(data_loader, model, loss_compute, device=None): "Standard Training and Logging Function" start = time.time() total_tokens = 0 total_loss = 0 tokens = 0 for i, batch in enumerate(data_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) decode_in = decode_in.to(device) decode_out = decode_out.to(device) decode_mask = decode_mask.to(device) ntokens = torch.sum(ntokens).to(device) out = model.forward(img_input, decode_in, encode_mask, decode_mask) # out --> [bs, 20, 512] 预测结果 # decode_out --> [bs, 20] 实际结果 # ntokens --> 标签中实际有效字符 loss = loss_compute(out, decode_out, ntokens) # 损失计算 total_loss += loss total_tokens += ntokens tokens += ntokens if i % 50 == 1: elapsed = time.time() - start print("Epoch Step: %d Loss: %f Tokens per Sec: %f" % (i, loss / ntokens, tokens / elapsed)) start = time.time() tokens = 0 return total_loss / total_tokens class SimpleLossCompute: "A simple loss compute and train function." def __init__(self, generator, criterion, opt=None): self.generator = generator self.criterion = criterion self.opt = opt def __call__(self, x, y, norm): """ norm: loss的归一化系数,用batch中所有有效token数即可 """ # x --> out --> [bs, 20, 512] 预测结果 # y --> decode_out --> [bs, 20] 实际结果 # norm --> ntokens --> 标签中实际有效字符 x = self.generator(x) # label smoothing需要对应维度变化 x_ = x.contiguous().view(-1, x.size(-1)) # [20bs, 512] y_ = y.contiguous().view(-1) # [20bs] loss = self.criterion(x_, y_) loss /= norm loss.backward() if self.opt is not None: self.opt.step() self.opt.optimizer.zero_grad() #return loss.data[0] * norm return loss.item() * norm4.5 贪心解码方便起见,我们使用最简单的贪心解码直接进行OCR结果预测。因为模型每一次只会产生一个输出,我们选择输出的概率分布中的最高概率对应的字符为本次预测的结果,然后预测下一个字符,这就是所谓的贪心解码,见代码中 greedy_decode() 函数。实验中分别将每一张图像作为模型的输入,逐张进行贪心解码统计正确率,并最终给出了训练集和验证集各自的预测准确率。# 训练结束,使用贪心的解码方式推理训练集和验证集,统计正确率 ocr_model.eval() print("\n------------------------------------------------") print("greedy decode trainset") total_img_num = 0 total_correct_num = 0 for batch_idx, batch in enumerate(train_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) # 获取单张图像信息 bs = img_input.shape[0] for i in range(bs): cur_img_input = img_input[i].unsqueeze(0) cur_encode_mask = encode_mask[i].unsqueeze(0) cur_decode_out = decode_out[i] # 贪心解码 pred_result = greedy_decode(ocr_model, cur_img_input, cur_encode_mask, max_len=sequence_len, start_symbol=1, end_symbol=2) pred_result = pred_result.cpu() # 判断预测是否正确 is_correct = judge_is_correct(pred_result, cur_decode_out) total_correct_num += is_correct total_img_num += 1 if not is_correct: # 预测错误的case进行打印 print("----") print(cur_decode_out) print(pred_result) total_correct_rate = total_correct_num / total_img_num * 100 print(f"total correct rate of trainset: {total_correct_rate}%") # 与训练集解码代码相同 print("\n------------------------------------------------") print("greedy decode validset") total_img_num = 0 total_correct_num = 0 for batch_idx, batch in enumerate(valid_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) bs = img_input.shape[0] for i in range(bs): cur_img_input = img_input[i].unsqueeze(0) cur_encode_mask = encode_mask[i].unsqueeze(0) cur_decode_out = decode_out[i] pred_result = greedy_decode(ocr_model, cur_img_input, cur_encode_mask, max_len=sequence_len, start_symbol=1, end_symbol=2) pred_result = pred_result.cpu() is_correct = judge_is_correct(pred_result, cur_decode_out) total_correct_num += is_correct total_img_num += 1 if not is_correct: # 预测错误的case进行打印 print("----") print(cur_decode_out) print(pred_result) total_correct_rate = total_correct_num / total_img_num * 100 print(f"total correct rate of validset: {total_correct_rate}%")greedy_decode() 函数实现。# greedy decode def greedy_decode(model, src, src_mask, max_len, start_symbol, end_symbol): memory = model.encode(src, src_mask) # ys代表目前已生成的序列,最初为仅包含一个起始符的序列,不断将预测结果追加到序列最后 ys = torch.ones(1, 1).fill_(start_symbol).type_as(src.data).long() for i in range(max_len-1): out = model.decode(memory, src_mask, Variable(ys), Variable(subsequent_mask(ys.size(1)).type_as(src.data))) prob = model.generator(out[:, -1]) _, next_word = torch.max(prob, dim = 1) next_word = next_word.data[0] next_word = torch.ones(1, 1).type_as(src.data).fill_(next_word).long() ys = torch.cat([ys, next_word], dim=1) next_word = int(next_word) if next_word == end_symbol: break #ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=1) ys = ys[0, 1:] return ys def judge_is_correct(pred, label): # 判断模型预测结果和label是否一致 pred_len = pred.shape[0] label = label[:pred_len] is_correct = 1 if label.equal(pred) else 0 return is_correct运行下面的命令即可一键开启训练:python ocr_by_transformer.py训练日志如下所示:epoch 0 train... Epoch Step: 1 Loss: 5.142612 Tokens per Sec: 852.649109 Epoch Step: 51 Loss: 3.064528 Tokens per Sec: 2709.471436 valid... Epoch Step: 1 Loss: 3.018526 Tokens per Sec: 1413.900391 valid loss: 2.7769546508789062 epoch 1 train... Epoch Step: 1 Loss: 3.440590 Tokens per Sec: 1303.567993 Epoch Step: 51 Loss: 2.711708 Tokens per Sec: 2743.414307 ... epoch 1499 train... Epoch Step: 1 Loss: 0.005739 Tokens per Sec: 1232.602783 Epoch Step: 51 Loss: 0.013249 Tokens per Sec: 2765.866211 ------------------------------------------------ greedy decode trainset ---- tensor([17, 32, 18, 19, 31, 50, 30, 10, 30, 10, 17, 32, 41, 55, 55, 55, 2, 0, 0, 0]) tensor([17, 32, 18, 19, 31, 50, 30, 10, 30, 10, 17, 32, 41, 55, 55, 55, 55, 55, 55, 55]) ---- tensor([17, 32, 18, 19, 31, 50, 30, 10, 17, 32, 41, 55, 55, 2, 0, 0, 0, 0, 0, 0]) tensor([17, 32, 18, 19, 31, 50, 30, 10, 17, 32, 41, 55, 55, 55, 55, 2]) total correct rate of trainset: 99.95376791493297% ------------------------------------------------ greedy decode validset ---- tensor([10, 11, 28, 27, 25, 11, 47, 45, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) tensor([10, 11, 28, 27, 25, 11, 62, 2]) ... tensor([20, 12, 24, 35, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) tensor([20, 12, 21, 12, 22, 23, 34, 2]) total correct rate of validset: 92.72088353413655%
-
如何将transformer引入OCR很多算法本身并不难,难的是如何思考和定义问题,把它转化到已知的解决方案上去。因此在看代码之前,我们先要聊聊,为什么transformer可以解决OCR问题,动机是什么?首先,我们知道,transformer被广泛应用在NLP领域中,可以解决类似机器翻译这样的sequence to sequence类的问题,如下图所示:而OCR识别任务,如下图所示,我们希望将下图识别为"Share",本质上也可以看作是一个sequence to sequence任务,只不过输入的序列信息是由图片形式表示的。因此,如果从把OCR问题看作是一个sequence to sequence预测问题这个角度,使用transformer解决OCR问题貌似是一个非常自然和顺畅的想法,剩下的问题只是如何将图片的信息构造成transformer想要的,类似于 word embedding 形式的输入。回到我们的任务,既然待预测的图片都是长条状的,文字基本都是水平排列,那么我们将特征图沿水平方向进行整合,得到的每一个embedding可以认为是图片纵向的某个切片的特征,将这样的特征序列交给transformer,利用其强大的attention能力来完成预测。因此,基于以上分析,我们将模型框架的pipeline定义为下图所示的形式:通过观察上图可以发现,整个pipeline和利用transformer训练机器翻译的流程是基本一致的,之间的差异主要是多了借助一个CNN网络作为backbone提取图像特征得到input embedding的过程。关于构造transformer的输入embedding这部分的设计,是本文的重点,也是整个算法能够work的关键。后文会结合代码,对上面示意图中展示的相关细节进行展开讲解。
-
数据集图像尺寸分析在进行图像分类检测等任务时,经常会查看图像的尺寸分布,进而确定合适的图像的预处理方式,例如在进行目标检测时会对图像尺寸和bounding box的尺寸进行统计,分析长宽比进而选择合适的图像裁剪策略和适当的初始anchor策略等。因此这里通过分析图像宽度、高度和宽高比等信息来了解数据的特点,为后续实验策略制定提供参考。def read_gas_image(data): with data.open() as fp: image = Image.open(fp) return image # 分析数据集图片尺寸 print("分析数据集图片尺寸:") # 初始化参数 min_h = 1e10 min_w = 1e10 max_h = -1 max_w = -1 min_ratio = 1e10 max_ratio = 0 # 遍历数据集计算尺寸信息 for data in tqdm.tqdm(train_segment): img = read_gas_image(data) # 读取图片 w, h = img.size # 提取图像宽高信息 ratio = w / h # 宽高比 min_h = min_h if min_h <= h else h # 最小图片高度 max_h = max_h if max_h >= h else h # 最大图片高度 min_w = min_w if min_w <= w else w # 最小图片宽度 max_w = max_w if max_w >= w else w # 最大图片宽度 min_ratio = min_ratio if min_ratio <= ratio else ratio # 最小宽高比 max_ratio = max_ratio if max_ratio >= ratio else ratio # 最大宽高比 # 输出信息 print('min_h:', min_h) print('max_h:', max_h) print('min_w:', min_w) print('max_w:', max_w) print('min_ratio:', min_ratio) print('max_ratio:', max_ratio)数据集图片尺寸相关情况统计结果如下:min_h: 9 max_h: 295 min_w: 16 max_w: 628 min_ratio: 0.6666666666666666 max_ratio: 8.619047619047619通过以上的结果,可看出图片多为卧倒的长条形,最大宽高比 > 8 可见存在极细长的图片。以上便是对于数据集的若干简单分析,并且准备出了训练要用的char2id映射文件,下面就是重头戏了,来看看我们如何将transfomer引入,来完成OCR单词识别这样的CV任务。
-
char和id的映射字典构建在本文OCR任务中,需要对图片中的每个字符进行预测,为了达到这个目的,首先就需要建立一个字符与其id的映射关系,将文本信息转化为可供模型读取的数字信息,这一步类似NLP中建立语料库。在构建映射关系时,除了记录所有标签文件中出现的字符外,还需要初始化三个特殊字符,分别用来代表一个句子起始符、句子终止符和填充(Padding)标识符(相关介绍戳这里)。后面dataset构建部分的讲解也还会再次提到。脚本运行后,所有字符的映射关系将会保存在 lbl2id_map.txt文件中。# 构造label中 字符--id 之间的映射 print("构造label中 字符--id 之间的映射:") lbl2id_map = dict() # 初始化三个特殊字符 lbl2id_map['☯'] = 0 # padding标识符 lbl2id_map['■'] = 1 # 句子起始符 lbl2id_map['□'] = 2 # 句子结束符 # 生成其余字符的id映射关系 cur_id = 3 for lbl in lbl_cnt_map.keys(): lbl2id_map[lbl] = cur_id cur_id += 1 # 保存 字符--id 之间的映射 到txt文件 with open(lbl2id_map_path, 'w', encoding='utf-8') as writer: # 参数encoding是可选项,部分设备并未默认为utf-8 for lbl in lbl2id_map.keys(): cur_id = lbl2id_map[lbl] print(lbl, cur_id) line = lbl + '\t' + str(cur_id) + '\n' writer.write(line)构造出的 字符-id 之间的映射:☯ 0 ■ 1 □ 2 C 3 A 4 ... = 85 ( 86 ) 87 + 88 é 89此外,analysis_recognition_dataset.py 文件中还包含一个建立关系映射字典的函数,可以通过读取含有映射关系txt的文件,构建出字符到id和id到字符的映射字典。这服务于后续transformer训练过程,以方便字符关系快速实现转换。def load_lbl2id_map(lbl2id_map_path): """ 读取 字符-id 映射关系记录的txt文件,并返回 lbl->id 和 id->lbl 映射字典 lbl2id_map_path : 字符-id 映射关系记录的txt文件路径 """ lbl2id_map = dict() id2lbl_map = dict() with open(lbl2id_map_path, 'r') as reader: for line in reader: items = line.rstrip().split('\t') label = items[0] cur_id = int(items[1]) lbl2id_map[label] = cur_id id2lbl_map[cur_id] = label return lbl2id_map, id2lbl_map
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签