2023年4月

Quartz 简单使用
Java SpringBoot 中,动态执行 bean 对象中的方法

源代码地址 =>
https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-quartz

工作原理解读

只要配置好 DataSource Quartz 会自动进行表的数据操作,

添加 Quartz Job 任务

保存 QRTZ_JOB_DETAILS、QRTZ_TRIGGERS => QRTZ_CRON_TRIGGERS

public void addJob(QuartzJob job) throws SchedulerException {
  ....
  JobDetail jobDetail = JobBuilder.newJob(jobClass)
                    .withIdentity(jobKey)
                    .build();
  // 放入参数,运行时的方法可以获取
  jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
  //该行代码执行后,会将定时任务插入 QRTZ_JOB_DETAILS 等相关表
  scheduler.scheduleJob(jobDetail, trigger);
  ....
}
//org.quartz.impl.jdbcjobstore.JobStoreSupport
public void storeJobAndTrigger(final JobDetail newJob, final OperableTrigger newTrigger) throws JobPersistenceException {
    this.executeInLock(this.isLockOnInsert() ? "TRIGGER_ACCESS" : null, new JobStoreSupport.VoidTransactionCallback() {
        public void executeVoid(Connection conn) throws JobPersistenceException {
            JobStoreSupport.this.storeJob(conn, newJob, false);  //数据保存 QRTZ_JOB_DETAILS 表
            JobStoreSupport.this.storeTrigger(conn, newTrigger, newJob, false, "WAITING", false, false); //数据保存 QRTZ_TRIGGERS 表
        }
    });
}

public int insertTrigger(...){
 INSERT_TRIGGER
 insertExtendedTriggerProperties => INSERT_CRON_TRIGGER OR INSERT_BLOB_TRIGGER

}

详见:
org.quartz.impl.jdbcjobstore.StdJDBCDelegate
将 job.getJobDataMap(),对像序列化后,存入
JOB_DETAILS.JOB_DATA
字段,可以是一个对像,以执行定时任务时,会把该字段反序列化,根据前期设定的内容进行业务处理
image

获取 Quartz Job 任务

执行计划任务时,获取 Job Detail

QuartzSchedulerThread.run()
=> qsRsrcs.getJobStore().acquireNextTriggers()
=> txCallback.execute(conn)
=> JobStoreSupport.acquireNextTriggers()
=> JobStoreSupport.retrieveJob()
=> StdJDBCDelegate.selectJobDetail()

image

删除 Quartz Job 任务

/**
 * <p>
 * Delete the base trigger data for a trigger.
 * </p>
 * 
 * @param conn
 *          the DB Connection
 * @return the number of rows deleted
 */
public int deleteTrigger(Connection conn, TriggerKey triggerKey) throws SQLException {
    PreparedStatement ps = null;

    deleteTriggerExtension(conn, triggerKey);
    
    try {
        ps = conn.prepareStatement(rtp(DELETE_TRIGGER));
        ps.setString(1, triggerKey.getName());
        ps.setString(2, triggerKey.getGroup());

        return ps.executeUpdate();
    } finally {
        closeStatement(ps);
    }
}

清除数据

/**
 * 清任务顺序
 */
public void clearData(Connection conn)
    throws SQLException {

    PreparedStatement ps = null;

    try {
        ps = conn.prepareStatement(rtp(DELETE_ALL_SIMPLE_TRIGGERS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_SIMPROP_TRIGGERS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_CRON_TRIGGERS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_BLOB_TRIGGERS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_TRIGGERS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_JOB_DETAILS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_CALENDARS));
        ps.executeUpdate();
        ps.close();
        ps = conn.prepareStatement(rtp(DELETE_ALL_PAUSED_TRIGGER_GRPS));
        ps.executeUpdate();
    } finally {
        closeStatement(ps);
    }
}

Demo 代码

MySQL 脚本

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql.sql

清除数据

DELETE FROM qrtz_simple_triggers ;
DELETE FROM qrtz_simprop_triggers ;
DELETE FROM qrtz_cron_triggers ;
DELETE FROM qrtz_blob_triggers ;
DELETE FROM qrtz_triggers ;
DELETE FROM qrtz_job_details ;
DELETE FROM qrtz_calendars ;
DELETE FROM qrtz_paused_trigger_grps ; 
DELETE FROM qrtz_scheduler_state ; 
DELETE FROM qrtz_locks ; 
DELETE FROM qrtz_fired_triggers

Pom.xml
如果SpringBoot版本是2.0.0以后的,则在spring-boot-starter中已经包含了quart的依赖,则可以直接使用spring-boot-starter-quartz依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--Quartz 集成需要和数据库交互-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.0.8</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.20</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--hutool 工具类-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.7</version>
</dependency>

image
QuartzJob 参考上图,建立实体

点击查看代码
package com.vipsoft.web.entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;

/**
 * 定时任务调度
 */
public class QuartzJob implements Serializable {

    private static final long serialVersionUID = -6798153039624729495L;

    /**
     * 任务序号
     */
    private int jobId;

    /**
     * 任务名称
     */
    @NotBlank(message = "任务名称不能为空")
    @Size(max = 10, message = "任务名称不能超过10个字符")
    private String jobName;

    /**
     * 任务组名
     */
    @NotBlank(message = "任务组名不能为空")
    @Size(max = 10, message = "任务组名不能超过10个字符")
    private String jobGroup;

    /**
     * 调用目标字符串
     */
    private String invokeTarget;

    /**
     * 执行表达式
     */
    private String cronExpression;

    /**
     * cron计划策略 0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行
     */
    private String misfirePolicy = "0";

    /**
     * 并发执行 0=允许,1=禁止
     */
    private String concurrent;

    /**
     * 描述 -- 任务说明
     */
    private String description;


    /**
     * 任务状态(0正常 1暂停)
     */
    private String status;


    public int getJobId() {
        return jobId;
    }

    public void setJobId(int jobId) {
        this.jobId = jobId;
    }

    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public String getJobGroup() {
        return jobGroup;
    }

    public void setJobGroup(String jobGroup) {
        this.jobGroup = jobGroup;
    }

    public String getInvokeTarget() {
        return invokeTarget;
    }

    public void setInvokeTarget(String invokeTarget) {
        this.invokeTarget = invokeTarget;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }

    public String getMisfirePolicy() {
        return misfirePolicy;
    }

    public void setMisfirePolicy(String misfirePolicy) {
        this.misfirePolicy = misfirePolicy;
    }

    public String getConcurrent() {
        return concurrent;
    }

    public void setConcurrent(String concurrent) {
        this.concurrent = concurrent;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

核心代码:QuartzJobServiceImpl

点击查看代码
package com.vipsoft.web.service.impl;

import cn.hutool.core.util.StrUtil;
import com.vipsoft.web.config.ScheduleConstants;
import com.vipsoft.web.entity.QuartzJob;
import com.vipsoft.web.exception.CustomException;
import com.vipsoft.web.job.CommonJob;

import com.vipsoft.web.service.IQuartzJobService;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Service
public class QuartzJobServiceImpl implements IQuartzJobService {

    @Autowired
    Scheduler scheduler;


    /**
     * 新增任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void clearAll(QuartzJob job) throws SchedulerException {
        scheduler.clear();
    }
    /**
     * 新增任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void addJob(QuartzJob job) throws SchedulerException {
        if (StrUtil.isEmpty(job.getStatus())) {
            // 如果没值,设置暂停
            job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
        }
        Class<? extends Job> jobClass = CommonJob.class;
        // 构建job信息
        int jobId = job.getJobId();
        String jobName = job.getJobName();
        String jobGroup = job.getJobGroup();
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);

        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobKey)
                .build();
        // 放入参数,运行时的方法可以获取
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);



        // 表达式调度构建器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job.getMisfirePolicy(), cronScheduleBuilder);

        // 按新的cronExpression表达式构建一个新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .withSchedule(cronScheduleBuilder)
                .build();

        // 判断是否存在
        if (scheduler.checkExists(jobKey)) {
            // 防止创建时存在数据问题 先移除,然后在执行创建操作
            scheduler.deleteJob(jobKey);
        }

        scheduler.scheduleJob(jobDetail, trigger);

        // 暂停任务
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
            scheduler.pauseJob(jobKey);
        }
    }

    /**
     * 设置定时任务策略
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(String misfirePolicy, CronScheduleBuilder cb)  {
        switch (misfirePolicy) {
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                throw new CustomException(60001, "策略配置异常 " + misfirePolicy);
        }
    }

    /**
     * 更新任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void updateJob(QuartzJob job) throws SchedulerException {
        // 判断是否存在
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        if (scheduler.checkExists(jobKey)) {
            // 防止创建时存在数据问题 先移除,然后在执行创建操作
            scheduler.deleteJob(jobKey);
        }
        addJob(job);
    }

    /**
     * 删除任务
     *
     * @param job
     * @Date 2016年1月16日
     * @since 2.0.0
     */
    @Override
    public void deleteJob(QuartzJob job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        if (this.scheduler.checkExists(jobKey)) {
            this.scheduler.deleteJob(jobKey);
        }
    }

    /**
     * 立即运行任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void run(QuartzJob job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        scheduler.triggerJob(jobKey);
    }

    /**
     * 暂停任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void pauseJob(QuartzJob job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        this.scheduler.pauseJob(jobKey);
    }

    /**
     * 恢复任务
     *
     * @param job 调度信息
     * @return 结果
     */
    @Override
    public void restartJob(QuartzJob job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        this.scheduler.resumeJob(jobKey);
    }

    /**
     * 获取quartz调度器的计划任务
     *
     * @param job 调度信息
     * @return 调度任务集合
     */
    @Override
    public List<QuartzJob> listJob(QuartzJob job) throws SchedulerException {
        List<QuartzJob> scheduleJobVOList = new ArrayList<>();
        GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
        Set<JobKey> jobKeys = this.scheduler.getJobKeys(matcher);
        for (JobKey jobKey : jobKeys) {
            List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
            for (Trigger trigger : triggers) {
                JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
                QuartzJob scheduleJobVO = (QuartzJob) jobDetail.getJobDataMap().get(ScheduleConstants.TASK_PROPERTIES);
                Trigger.TriggerState triggerState = this.scheduler.getTriggerState(trigger.getKey());
                scheduleJobVO.setStatus(triggerState.name());
                // 判断trigger
                if (trigger instanceof SimpleTrigger) {
                    SimpleTrigger simple = (SimpleTrigger) trigger;
                    scheduleJobVO.setCronExpression("重复次数:" + (simple.getRepeatCount() == -1 ? "无限" : simple.getRepeatCount()) + ",重复间隔:"
                            + (simple.getRepeatInterval() / 1000L));
                    scheduleJobVO.setDescription(simple.getDescription());
                }
                if (trigger instanceof CronTrigger) {
                    CronTrigger cron = (CronTrigger) trigger;
                    scheduleJobVO.setCronExpression(cron.getCronExpression());
                    scheduleJobVO.setDescription(cron.getDescription() == null ? ("触发器:" + trigger.getKey()) : cron.getDescription());
                }
                scheduleJobVOList.add(scheduleJobVO);
            }
        }
        return scheduleJobVOList;
    }
}

ScheduleConfig

点击查看代码
package com.vipsoft.web.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import javax.sql.DataSource;
import java.util.Properties;

@Configuration
public class ScheduleConfig {

    /**
     * 设置属性
     */
    private Properties quartzProperties() {
        // quartz参数
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "VipSoftScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        // 线程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20");
        prop.put("org.quartz.threadPool.threadPriority", "5");
        // JobStore配置
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        // 集群配置
        prop.put("org.quartz.jobStore.isClustered", "true");
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");

        // sqlserver 启用
        // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        return prop;
    }


    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);

//        //获取配置属性--通过加载配置文件的方式获取配置
//        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
//        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
//        //在quartz.properties中的属性被读取并注入后再初始化对象
//        propertiesFactoryBean.afterPropertiesSet();
//        factory.setQuartzProperties(propertiesFactoryBean.getObject());

        //用于quartz集群,加载quartz数据源配置
        factory.setQuartzProperties(this.quartzProperties());

        factory.setSchedulerName("VipSoftScheduler");
        //QuartzScheduler 延时启动,应用启动完10秒后 QuartzScheduler 再启动
        factory.setStartupDelay(10);
        // 可选,QuartzScheduler
        // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        // 设置自动启动,默认为true
        factory.setAutoStartup(true);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");

        return factory;
    }
}

暂停后,qrtz_triggers 表的 TRIGGER_STATE = PAUSED
image
运行效果
http://localhost:8088/schedule/add
http://localhost:8088/schedule/pause
http://localhost:8088/schedule/restart

{"jobName":"测试","jobGroup":"DEFAULT","invokeTarget":"scheduletask.execute('VipSoft Quartz')","cronExpression":"0/10 * * * * ?","misfirePolicy":2,"concurrent":1,"status":"0"}

image

参考

实现方式参考:若依(RuoYi),他是新建了一张中间表,通过 init() 方法,利用中心中间表进行定时任务的初始化

/**
 * 项目启动时,初始化定时器 
 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
 */
@PostConstruct
public void init() throws SchedulerException, TaskException
{
    scheduler.clear();
    List<SysJob> jobList = jobMapper.selectJobAll();
    for (SysJob job : jobList)
    {
        ScheduleUtils.createScheduleJob(scheduler, job);
    }
}

深度学习基础5:交叉熵损失函数、MSE、CTC损失适用于字识别语音等序列问题、Balanced L1 Loss适用于目标检测

1.交叉熵损失函数

在物理学中,“熵”被用来表示热力学系统所呈现的无序程度。香农将这一概念引入信息论领域,提出了“信息熵”概念,通过对数函数来测量信息的不确定性。交叉熵(cross entropy)是信息论中的重要概念,主要用来度量两个概率分布间的差异。假定 p和 q是数据 x的两个概率分布,通过 q来表示 p的交叉熵可如下计算:

$H\left(p,q\right)=-\sum\limits_{x}p\left(x\right)\log q\left(x\right)$

交叉熵刻画了两个概率分布之间的距离,旨在描绘通过概率分布 q来表达概率分布 p的困难程度。根据公式不难理解,交叉熵越小,两个概率分布 p和 q越接近。

这里仍然以三类分类问题为例,假设数据 x属于类别 1。记数据x的类别分布概率为 y,显然 y=(1,0,0)代表数据 x的实际类别分布概率。记$\hat{y}$代表模型预测所得类别分布概率。那么对于数据 x而言,其实际类别分布概率 y和模型预测类别分布概率 $\hat{y}$的交叉熵损失函数定义为:

$cross entryy=-y\times\log(\hat{y})$

很显然,一个良好的神经网络要尽量保证对于每一个输入数据,神经网络所预测类别分布概率与实际类别分布概率之间的差距越小越好,即交叉熵越小越好。于是,可将交叉熵作为损失函数来训练神经网络。

图1 三类分类问题中输入x的交叉熵损失示意图(x 属于第一类)

在上面的例子中,假设所预测中间值 (z1,z2,z3)经过 Softmax映射后所得结果为 (0.34,0.46,0.20)。由于已知输入数据 x属于第一类,显然这个输出不理想而需要对模型参数进行优化。如果选择交叉熵损失函数来优化模型,则 (z1,z2,z3)这一层的偏导值为 (0.34−1,0.46,0.20)=(−0.66,0.46,0.20)。

可以看出,$Softmax$和交叉熵损失函数相互结合,为偏导计算带来了极大便利。偏导计算使得损失误差从输出端向输入端传递,来对模型参数进行优化。在这里,交叉熵与Softmax函数结合在一起,因此也叫 $Softmax$损失(Softmax with cross-entropy loss)。

2.均方差损失(Mean Square Error,MSE)

均方误差损失又称为二次损失、L2损失,常用于回归预测任务中。
均方误差函数通过计算预测值和实际值之间距离(即误差)的平方来衡量模型优劣
。即预测值和真实值越接近,两者的均方差就越小。

计算方式:假设有 n个训练数据 $x_i$,每个训练数据 $x_i$ 的真实输出为 $y_i$,模型对 $x_i$的预测值为 $\hat{y_i}$。该模型在 n 个训练数据下所产生的均方误差损失可定义如下:

$MSE=\dfrac{1}{n}\sum\limits_{i=1}
n\left(y_i-\hat{y}_i\right)
2$

假设真实目标值为100,预测值在-10000到10000之间,我们绘制MSE函数曲线如 图1 所示。可以看到,当预测值越接近100时,MSE损失值越小。MSE损失的范围为0到∞。

3.CTC损失

3.1 CTC算法算法背景-----文字识别语音等序列问题

CTC 算法主要用来解决神经网络中标签和预测值无法对齐的情况

通常用于文字识别以及语音等序列学习领域
。举例来说,在语音识别任务中,我们希望语音片段可以与对应的文本内容一一对应,这样才能方便我们后续的模型训练。但是对齐音频与文本是一件很困难的事,如 图1 所示,每个人的语速都不同,有人说话快,有人说话慢,我们很难按照时序信息将语音序列切分成一个个的字符片段。而手动对齐音频与字符又是一件非常耗时耗力的任务

图1 语音识别任务中音频与文本无法对齐

在文本识别领域,由于字符间隔、图像变形等问题,相同的字符也会得到不同的预测结果,所以同样会会遇到标签和预测值无法对齐的情况。如 图2 所示。

图2 不同表现形式的相同字符示意图

总结来说,假设我们有个输入(如字幅图片或音频信号)X ,对应的输出是 Y,在序列学习领域,通常会碰到如下难点:

  • X和 Y都是变长的;

  • X和 Y的长度比也是变化的;

  • X和 Y相应的元素之间无法严格对齐。

3.2 算法概述

引入CTC主要就是要解决上述问题。这里以文本识别算法CRNN为例,分析CTC的计算方式及作用。CRNN中,整体流程如 图3 所示。

图3 CRNN整体流程

CRNN中,首先使用CNN提取图片特征,特征图的维度为$m×T$,特征图 x可以定义为:

$x=(x
1,x
2,...,x^T)\quad\text{}$

然后,将特征图的每一列作为一个时间片送入LSTM中。令 t为代表时间维度的值,且满足 $1<t<T$,则每个时间片可以表示为:

$x
t=(x_1
t,x_2
t,\ldots,x_m
t)$

经过LSTM的计算后,使用softmax获取概率矩阵 y,定义为:

$y=(y
1,y
2,\ldots,y^T)$

经过LSTM的计算后,使用softmax获取概率矩阵 $y^t$,定义为:

$y
t=(y_1
t,y_2
t,\ldots,y_n
t)$

n为字符字典的长度,由于 $y_i^t$是概率,所以$\Sigma_i y_i^t=1$ 。对每一列 $y^t$求 argmax(),就可以获取每个类别的概率。

考虑到文本区域中字符之间存在间隔,也就是有的位置是没有字符的,所以这里定义分隔符 −来表示当前列的对应位置在图像中没有出现字符。用 $L$代表原始的字符字典,则此时新的字符字典 $L′$为:

$L'=L\cup{-}$

此时,就回到了我们上文提到的问题上了,由于字符间隔、图像变形等问题,相同的字符可能会得到不同的预测结果。在CTC算法中,定义了 B变换来解决这个问题。 B变换简单来说就是将模型的预测结果去掉分割符以及重复字符(如果同个字符连续出现,则表示只有1个字符,如果中间有分割符,则表示该字符出现多次),使得不同表现形式的相同字符得到统一的结果。如 图4 所示。

这里举几个简单的例子便于理解,这里令T为10:

$\begin{array}{c}B(-s-t-aative)=state\ \ B(ss-t-a-t-e)=state\ \ B(sstt-aat-e)=state\end{array}$

对于字符中间有分隔符的重复字符则不进行合并:

$B(-s-t-t state)=state$

当获得LSTM输出后,进行 B变换就可以得到最终结果。由于 B变换并不是一对一的映射,例如上边的3个不同的字符都可以变换为state,所以在LSTM的输入为 x的前提下,CTC的输出为 l的概率应该为:

$p(l|x)=\Sigma_{\pi\in B^{-1}(l)}p(\pi|x)$

其中, $pi$为LSTM的输出向量, $\pi\in B^{-1}(l)$代表所有能通过 B变换得到 l的 $pi$的集合。

而对于任意一个 π,又有:

$p(\pi|x)=\Pi_{t=1}^T y_{\pi_t}^t$

其中, $y_{\pi_t}^t$代表 t时刻 π为对应值的概率,这里举一个例子进行说明:

$\begin{array}{c}\pi=-s-t-aattte\ y_{\pi_t}
t=y_-
1
y_s 2*y_- 3
y_t
4*y_-
5
y_a 6*y_a 7
y_t
8*y_t
9*y_e^10\ \end{array}$

不难理解,使用CTC进行模型训练,本质上就是希望调整参数,使得$p(\pi\text{}|x)$ 取最大。

具体的参数调整方法,可以阅读以下论文进行了解:
Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks

4.平衡 L1损失(Balanced L1 Loss)---目标检测

目标检测(object detection)的损失函数可以看做是一个多任务的损失函数,分为分类损失和检测框回归损失:

$L_{p,u,tu,v}=L_{cls}(p,u)+\lambda[u\geq1]L_{loc}(t^u,v)$

$L_cls$表示分类损失函数、$L_loc$表示检测框回归损失函数。在分类损失函数中,p表示预测值,u表示真实值。$t_u$表示类别u的位置回归结果,v是位置回归目标。λ用于调整多任务损失权重。定义损失大于等于1.0的样本为outliers(困难样本,hard samples),剩余样本为inliers(简单样本,easy sample)。

平衡上述损失的一个常用方法就是调整两个任务损失的权重,然而,回归目标是没有边界的,直接增加检测框回归损失的权重将使得模型对outliers更加敏感,这些hard samples产生过大的梯度,不利于训练。inliers相比outliers对整体的梯度贡献度较低,相比hard sample,平均每个easy sample对梯度的贡献为hard sample的30%,基于上述分析,提出了balanced L1 Loss(Lb)。

Balanced L1 Loss受Smooth L1损失的启发,Smooth L1损失通过设置一个拐点来分类inliers与outliers,并对outliers通过一个$max(p,1.0)$进行梯度截断。相比smooth l1 loss,Balanced l1 loss能显著提升inliers点的梯度,进而使这些准确的点能够在训练中扮演更重要的角色。设置一个拐点区分outliers和inliers,对于那些outliers,将梯度固定为1,如下图所示:

Balanced L1 Loss的核心思想是提升关键的回归梯度(来自inliers准确样本的梯度),进而平衡包含的样本及任务。从而可以在分类、整体定位及精确定位中实现更平衡的训练,Balanced L1 Loss的检测框回归损失如下:

$L_{loc}=\sum\limits_{i\in x,y,w,h}L_b(t_i^u-v_i)$

其相应的梯度公示如下:

$\dfrac{\partial L_{loc}}{\partial w}\propto\dfrac{\partial L_b}{\partial t_i^u}\propto\dfrac{\partial L_b}{\partial x}$

基于上述公式,设计了一种推广的梯度公式为:

$\dfrac{\partial L_b}{\partial x}=\begin{cases}\alpha ln(b|x|+1),if|x|<1\ \gamma,otherwise\end{cases}$

其中,$α$控制着inliers梯度的提升;一个较小的α会提升inliers的梯度同时不影响outliers的值。$γ$来调整回归误差的上界,能够使得不同任务间更加平衡。α,γ从样本和任务层面控制平衡,通过调整这两个参数,从而达到更加平衡的训练。Balanced L1 Loss公式如下:

$L_b(x)=\begin{cases}\frac ab(b|x|+1)ln(b|x|+1)-\alpha|x|,if|x|<1\ \gamma|x|+C,otherwise\end{cases}$

其中参数满足下述条件:

$\alpha ln(b|x|+1)=\gamma\quad\text{}$

默认参数设置:α = 0.5,γ=1.5

Libra R-CNN: Towards Balanced Learning for Object Detection

提前声明:
我没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现;

解决了react的非父子间的通信;

参考文档:
https://github1s.com/browserify/events/blob/main/events.js

https://www.npmjs.com/package/events

https://github.com/browserify/events

1.其中的一种实现的方式

首先先新建一个文件eventBus.tsx

class EventBus {
constructor() {
this.events = this.events || new Map(); // 储存事件/回调键值对
this.maxListeners = this.maxListeners || 10; // 设立监听上限
}

// 触发名为type的事件
emit(type, ...args) {
let handler = null;
handler = this.events.get(type);
console.log('

C语言

编程重要的是思维而不是语法

在开始学习编程之前,首先我们要先明确一件事,就是学编程重要的是思维而不是语法。
语言只是工具,思维才是本质。
工具只是便捷你的工作与提高你的效率,但真正决定你能否解决问题的是你的思维方式.
就好比做几何题的时候,垂直平行等那套数学符号语言(工具)真的重要吗?
那只是一种表述方式而已,不会那套数学符号还可以写文字描述和算式。
决定你最终能否解答这道题的还是你是否有解题的思路。

编程也是如此。

模块化概念

在编程学习开始之前,我们还需要了解一个概念,就是模块化。

c语言是一个模块化的语言,这个模块化体现在很多方面,比如函数,比如结构体,比如多文件。
一个c语言程序,就是由一个个变量拼成结构体,一个个结构体与函数拼成文件,最后再由一个个文件拼成最后的整个程序

任何一个C语言程序都是由一个或者多个程序段构成,每个程序段分别负责各自的功能,最后由主程序段统合到一起形成可以执行的程序。 ——这种负责某一部分功能的程序段我们通常称之为“函数”。

主函数

电脑计算机执行程序总要有个开始,总要有个第一行。
前面提到过c语言中每个程序段分别负责各自的功能,那么计算机执行的时候又怎么能知道先执行哪里后执行哪里呢?
为了解决这一问题,我们需要有一段程序来做各个程序段的统合

这个程序也就是
main
函数
也就是说,如果你写的程序要运行的话,一定要有一个main函数
那么
main
函数如何写呢?
为了解决这一问题,我们需要了解一下函数的结构。

函数的结构

前面提到,一个c语言程序,就是由一个个变量拼成结构体,一个个结构体与函数拼成文件,最后再由一个个文件拼成最后的整个程序。

那么函数是怎么样的呢?这里我们以不可或缺的主函数为例

void main()
{    
    //代码内容    
}

这就是一个最简化的函数(当然我们通常不用void,这里只是为了方便理解)

这个函数的结构是这样的

void main()   // 返回值类型 函数名(参数列表)       表示创建一个函数
    {    //代码块开始
    	//代码块可以简单理解为我们要运行的一段代码
	}	//代码块结束

这里的void是指返回值的类型,void表示没有返回值,为什么函数需要返回值呢?因为我们在执行一个函数的时候通常是需要它来实现某个功能的,如果没有返回值我们就不知道它有没有成功执行,或者不知道它的执行结果了(譬如执行开平方以后我们没有收到返回值,平方是开完了,但是结果捏?????)

而且有些时候我们的系统在执行函数的时候也强制要求返回值(一些系统,不是所有系统)

综上,我们的程序最好提供一个返回值

因此在事实上我们的一个函数表达出来其实是这样的

类型名 main(){
    //函数内容    
    return 数值;
}

这里的类型名常用的有 int char double long short float bool

分别对应为整数,字符,双精度浮点数,长数,短数,浮点数,浮点数就是带小数点的数,计算机处理浮点数有误差,double比float精准一些

这里我们所写的函数名字叫main,也就是前文提到的主函数

由于是主函数,所以我们的返回值自己是没有办法用到了,但是有的计算机系统可能会要求,因此通常的写法是返回一个0,代表程序正确执行完成,此时我们的程序就变成了

int main()    //创建一个返回值为整数的函数 名为main 传入参数列表为空
{
    //函数内容 
    return 0;  //  返回整数 0 
}

这里我们运行一下这个程序给大家看看结果

(展示)因为我们函数内容什么都没有写,所以也什么都没有显示

那么怎么证明我们的程序真的执行成功了呢?我们再增加一行用于显示的指令

#include <stdio.h>

int main()    //创建一个返回值为整数的函数 名为main 传入参数列表为空
{
    printf("hello world");//显示hello world
    return 0;  //  返回整数 0 
}

注意!C语言函数里的语句结束后需要以英文分号结尾,每一句都需要

printf是c语言里的打印(显示到屏幕)语句,这句话的意思是显示hello world

加上之后我们再来运行一下(展示)程序就会显示输出hello world
加上以后我们的命令后就显示出了我们要显示的内容,这证明本喵刚才认真讲了没有胡说八道(骄傲)

可能刚才有银还意识到一个问题,就是刚才的程序比上面说的多了一行

#include <stdio.h>

这一句的意思又是什么呢?

后面加的字符是c语言里的预处理指令,就是编译器程序执行前预先处理的指令,用来补足程序运行中需要的一些东西

这个#include就是包含头文件的意思

#include <stdio,h>

的意思就是这个程序要包含文件stdio.h里面的内容,stdio.h是c语言编译器自带的一个头文件,包含了输入输出的函数之类的一些常用的功能函数,printf()其实也是一个函数,它在stdio文件里面,我们使用的printf就是调用了程序外部文件stdio.h里面的print函数。

刚才咱们已经说了,c语言由很多函数构成,下面咱们再定义一下别的函数来个多函数的程序

#include 

int die()
{
    printf(" bay~ world " );	
    return 0;
}


int main()
{
    die();
    return 0;
}


由于这个不是主函数了所以函数名就可以自己起了,不过这里要注意,函数名不能和关键字(c语言里面已经被占用的词)同名

这里我们起名die

这里左下角就正确显示了

这里是先定义了die函数然后在主函数里面调用了它

die();

就酱,我们写了一个多个函数的c语言程序,(在学习编程的初期可以先不用考虑多文件,先从一个文件写起)

这里要注意!我们使用的程序要在使用的地方之前出现,例如dnlm函数在main函数之前

然后下面我们讲一坨选择分支结构

选择分支

这个选择分支结构它是这个样子的

if()
{
    //如果条件成立执行
}
else
{
    //如果条件不成立执行
}

这里我们添加一个丢乃老父并且声明母亲父亲两个整数来测试一下

#include 

int dnlm()
{	
    int a=30;//声明整数a等于30	
    int b=20;    //声明整数b等于20	
    if a>b)    //如果a大于b	
    {
        printf(" a" );    //显示	
    }
    else    //否则
    {
        printf(" b" );    //显示
    }	
    return 0;
}

int main()
{
    dnlm();
    return 0;
}


运行结果正确的显示在了左下角。

这样一来证明我们的选择结构发挥了它应有的效果

刚才讲完了选择分支结构,这次我们从循环结构开始讲,一课时之内把c语言入门讲完,然后看看时间能不能够讲一些其他的东西。

循环结构

首先是while循环,这是平常会经常用到的一个循环

while(循环条件){    //代码内容}

while循环的结构是这样的,首先判断循环条件是不是成立,例如循环条件是变量a>=2,那么当a大于等于2的时候,就会执行代码内容一次,然后再次判断a是不是还大于等于2,如果是的话再执行一次。直到有一次发现a不再大于等于2了,才结束循环开始执行循环外面的内容。这样一来的话,实际上也就是说,我们在写这种循环的时候,里面一定要写改变循环条件的代码,否则循环条件一直不变就变成死循环了。
while循环是先判断然后才决定执不执行的,所以while循环最少会执行0次,也就是说如果一开始条件就不成立的话就一次都不执行。
与之相对的是do。。。while循环

do
{
//代码内容
}while() 

do。。。while和while相反,dowhile是先做了再说,做一遍到做完以后再判断,是不是条件成立 ,条件成立的话继续循环,条件不成立的话到此为止,继续往下执行,do while循环是至少会执行一次的循环。

与while系列循环用的同样多的就是for循环了

for(i=100;i>=0;i--){    //代码内容}

for循环的条件分为三部分,第一部分是声明控制循环的变量,比如声明个i=100,第二个部分和while的控制条件一样是用来判断循环执不执行,最后一个部分是用来声明控制变量的变化,就和while一样,for也需要改变控制条件来防止死循环,不过for直接在控制条件里就可以写好改变控制变量的语句。

以上是循环结构的三种语句。

跳转语句

下面进行跳转语句

c语言里的跳转语句其实就是goto

语法也很简单,在任意一个地方立下flag,然后就可以随时goto到这个地方了

lable1:
  //此处省略两百万亿行
goto lable1

就可以直接飞回flag

goto用的好的话不仅可以跳转还可以实现各种循环,不过goto很容易出现不知道飞到了哪里去但是编译器不报错导致查错人员头比地球还大的现象,因此一般的来说,不建议使用goto语句。

数组

数组部分也很简单,其实就是把数据连起来存

我们平常声明一个变量a,可以储存一个数据。

我们现在声明一个数组a[100],就可以储存100个数据

数据类型 数组名[数组长度]

可以通过a[0-99]来分别使用这100个数据,就不用写100遍声明变量了,而且如果我们要查找的数有好几个特点,还可以使用二维或者多维数组。

比如a[1][2][3][4][5][6][7]就是七维数组里第二组的第三小组的第四小组的第五小组的第六小组的第七小组的第八个数

这里要注意
数组里面排序序号是从零开始的,第一个内容的编号是0,所以数组名加数字实际访问到位置是数字加一的位置存的数据。
例如
a[0]
其实是数组a的第一位。

从本质上而言,数组其实就是一种指针的应用方式。

指针

语言里面指针是核心精髓所在,所谓指针,就是指向数据储存的地方。

我们平常声明一个变量,a=100,大概就相当于在白纸上划了一部分叫做a区,然后往里面记录了一个数字100,100写在a这个地方。

这块a区所在的位置就叫做a的地址

c语言里面和地址有关的有两个符号,一个是
&
,取地址符,一个是
*
,是指针的标记(也是解引用符号)

我们平常创建变量的时候是这样的

int a;

我们创建指针变量时候是这样的

int* p;

标上一个*

表示这个变量p存储的是int变量的地址。

注意:指针和指针变量是不同的 !指针是地址,指针变量是存放指针的变量

我们平常给变量赋值的方法是这样的

a=100;

我们给指针变量赋值的时候是这样的

p=&a;

这里的&a的意思就是取得a的地址

看到这里大家可能会有所疑惑,这个指针和变量到底有什么区别呢?

这里举一个简单的例子来说明指针和直接调用变量的区别

坐在隔壁的小明想要抄你的试卷,他看了一下a区,把数字抄走了,这是b=a,

坐在隔壁的小明,他拿走了你的试卷,这是b=&a

前者只是拿走了数据,后者拿走的数据的地址,那么这会导致什么呢?

当小明(b)想要修改卷子的时候——

前者,b修改了卷子上的数值,a的卷子没事,因为b只是修改了抄走的一个数据

后者,b修改了卷子上的数值,a惊叫一声:卧槽你把我卷子改了干啥!!!

这就是使用变量的值和使用变量的指针的区别

BUT!!!为什么我们需要使用指针呢????

当然是因为
有些时候我们必须使用指针!

举个例子!比如我们想要在某个函数里修改函数外的值的时候,我们就必须得使用指针。

这里我们需要涉及一点关于函数调用、形参与实参的知识。

有些时候我们要用的函数会需要传入一些数值,比如我们写一个求和的函数

int sum(int a,int b)  //声明一个返回值是整数的函数,使用时需要传入整数a和整数b
{
	return a+b; //返回a+b的值
}

然后我们在使用的时候就需要传入两个值

sum(x,y) //x和y是之前已经弄好的存了值的变量
sum(10,20)//或者这样直接给两个数值

这里面的a和b就叫形参,也可以简单粗略地理解为参数所需的形式,x和y就是实参,可以简单粗略的理解为实际传入的参数。

在程序运行到调用sum函数的时候就会创建两个临时的变量a和b,然后把x和y的值传给a和b。

这里就出现了不使用指针无法解决的问题——如果我们想要把存储的结果还存在x里呢?

当然,这样其实还能解决,
x=sum(x,y)
就行了,但是如果我们要存到的地方不确定呢?比如根据sum计算结果的不同存到不同的地方,是不是没有办法啦!

而使用指针就可以很方便(才怪)的解决这个问题,我们不传入值,而是把地址传过去,小明不久可以直接修改你的试卷了吗?

#include <stdio.h>
int main(int argc, char const *argv[])
{
	int x=1;
	int y=2;
	sum (&x,&y);
	return 0;
}

int sum(int* a,int* b)
{
	*a=*a+*b;
	return 0;
}

这样就完美的解决了这个问题。

那么这里出一道小题,如果想要修改指针变量int* a的值应该怎么办呢?

int sum(int** a,int** b)
{
	*a=*a+*b;
	return 0;
}

当然是传入指针的指针!

指针是可以一层套一层的,
int************************************************ a
都可以!(当然,没什么大病的话一般是不会写太多层的)

回到刚才,讲指针的时候我们有说数组是指针的一种应用,是什么意思呢?现在我们就可以解答这个问题了,数组就是根据你的需求创建了好多挨在一起的空间,然后数组的名字就是一个指针,指向第一块地方,然后数组[]里跟不同的数字就是把指针往后移不同的长度,指到不同的地方。

比如a[3],其实也就是*(a+3)的意思,你把a[3]写成
*(a+3)
,
*(3+a)
,
3[a]
都可以的,都是一样的东西。

这里也解答了为什么数组前面要加int double之类的类型的问题,因为声明数组的时候需要创建出一些地方来存数据,不同类型数据大小不一眼需要的空间也不一样,数组前面的数据类型是用来标识每一块地方多大的。

追求技术是一场漫长的旅程,很高兴在这里与你们相逢。

前言

在从github/gitee远程仓库获取代码时,除了使用https方式,我们还可以使用ssh连接的方式与远程仓库服务器通信,其好处是有时会比https更方便、稳定、快速。

和与普通的linux服务器连接一样,为了免去每次远程操作都要输入用户名和密码的苦恼,我们可以选择使用 密钥对 的方式免密登录。

准备工作(win11)

使用git来clone项目有两种方式:HTTPS和SSH:

HTTPS:不管是谁,拿到url随便clone,但是在push的时候需要验证用户名和密码;
SSH:clone的项目你必须是拥有者或者管理员,而且需要在clone前添加SSH Key。SSH 在push的时候,是不需要输入用户名的,如果配置SSH key的时候设置了密码,则需要输入密码的,否则直接是不需要输入密码的。

https的方式比较好理解,本文以SSH为例来讲解Windows系统git使用ssh方式来gitee进行同步(
github
类似)。

Gitee 提供了基于SSH协议的Git服务,在使用SSH协议访问仓库之前,需要先配置好账户/仓库的SSH公钥。

1,如果是win10,则推荐本地安装windows terminal(
https://learn.microsoft.com/zh-cn/windows/terminal/install
)。
2,安装git。
3,有一个gitee账户并创建一个仓库。

步骤一,生成ssh私钥和公钥并配置gitee公钥

打开terminal,运行ssh-keygen
过程会提示文件保存路径、密码,不用输入,连续两次回车。

ssh-keygen

打开 C:\Users\当前用户.ssh 目录,会有两个文件id_rsa(私钥)id_rsa.pub(公钥)

这里千万注意,window自带的ssh客户端其配置目录位于C:\Users\当前用户.ssh,必须在这目录下。
id_rsa文件(私钥)
id_rsa.pub文件(公钥)

目录文件

记事本打开
id_rsa.pub
公钥文件,复制内容到gitee公钥里(
https://gitee.com/profile/sshkeys
),标题随便写。点确定保存。

gitee公钥配置

注意这里的指纹哈希sha256和上面命令行生成的指纹是一致的,证明复制正确

步骤二,配置ssh客户端(踩了很多坑)

window自带的ssh客户端其配置目录位于C:\Users\当前用户.ssh
确保该目录下的两个文件id_rsa(私钥),不能改名

测试ssh是否能连接成功,terminal运行:

ssh -T git@gitee.com

提示 You've successfully authenticated

ssh -T git@gitee.com

另:遇到的其他情况的处理,

问题处理

步骤三,复制ssh仓库url(非https)克隆代码

找到正确ssh地址

找到ssh地址

开始git clone仓库(clone完毕后git remote -v验证远程连接)

git clone

踩坑总结

主要是ssh客户端设置,window自带的ssh客户端,其config配置位于
C:\Users\当前用户.ssh
目录,ssh命令执行的时候依赖.ssh目录的配置,ssh-keygen默认生成(rsa加密)私钥文件id_rsa,一定位于.ssh文件夹之下,且不能改名!
如果id_rsa位于其他文件夹,例如d:\sshconfig\,那么运行ssh命令需要加 -i参数,不过可能遭遇WARNING: UNPROTECTED PRIVATE KEY FILE!,具体解决方法可以搜索解决

ssh -T git@gitee.com -i d:\sshconfig\

i参数说明:
-i identity_file A file from which the identity key (private key) for public key authentication is read.

参考资料:

更多关于win10 ssh客户端的说明,参考:
https://learn.microsoft.com/zh-cn/windows/terminal/tutorials/ssh
更多关于ssh命令的参考:
https://www.ssh.com/academy/ssh/command
更多关于ssh-keygen命令的参考:
https://www.ssh.com/academy/ssh/keygen
生成/添加SSH公钥:
https://gitee.com/help/articles/4181


前往了解
国思RDIF
低代码开发平台:
www.guosisoft.com
国思RDIF低代码开发平台