[C4-AI2022]基于飞桨的手语翻译与辅助教学系统

★★★ 本文源自AI Studio社区精品项目,【点击此处】查看更多精品内容 >>>


基于飞桨的手语翻译与辅助教学系统

项目概述

世卫组织统计,现今有1.5亿人患有听力障碍,而到2030年这一数字将上升到2.5亿。手语是听障人群的主要沟通方式。但是在社交、医疗和工作等行业的手语服务需求却被长久忽视。我国手语行业发展缓慢并且手语教育普及度不够,手语教育行业人才紧缺成为目前亟需解决的社会问题。除此之外,手语自身存在地区差异,而标准手语推广不足。与口语相比,手语更关注话题,存在明显语序不同。两者之间需要逻辑转换,也即手语翻译。作为视觉语言,手语很难从简单文本描述中清晰学习。这些都给手语学习带来了困难。

当前市场上的手语服务平台只提供手语知识查询功能。手语教学多以长视频为主,不支持碎片化学习和查询学习。
为了改善听障人士的生活、工作和学习,本项目利用人工智能技术研发服务于听障人群和公共听力无障碍设施的手语辅助教学与翻译系统。系统旨在实现如下目标:

  • 基于自然语言处理和图像处理算法完成手语翻译功能
  • 基于手语翻译功能并结合手语纠错算法完成手语教学功能
  • 基于数据库技术构建手语知识库,丰富手语翻译和教学功能。

图1 手语翻译与辅助教学项目框架

图1展示了本项目主要框架,包括场景应用、服务功能算法核心三个层次。其中场景应用面向个人和公共领域开放系统服务接口,用户能够通过接口向系统输入口语或手语,并获取相应的系统反馈结果。在算法核心层,项目对经过数据处理后的口语或手语进行逻辑转换,转换结果将结合手语知识库中的信息送至服务功能层进行整理并展示。

项目平台前端展示如视频所示:

scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true">

项目技术方案

系统主要架构如图2所示,主要包括手语与口语双向翻译和手语辅助教学。手语与口语双向翻译包括口语到手语转换及手语到口语转换。口语转换手语以口语语音或文本为输入,通过自然语言处理模型将口语逻辑转换为对应手语逻辑,并将转换结果以文字和视频形式呈现。手语转换口语则执行相反的逻辑转换过程,系统接收手语动作视频为输入并输出口语文字或语音。其中手语动作视频中的手语提取由手语识别功能完成。系统除了提供手语与口语双向翻译结果,还会通过查询手语知识库,将与翻译结果关联的手语知识一并展现。相比于传统手语服务平台,本项目的主要创新点如下:

  • 针对现有手语翻译只能提供手语单词查询的问题,本系统实现连续手语翻译。输入连续口语,系统能够翻译出对应的手语句子,并以视频形式呈现。
  • 针对现有手语服务平台只提供手语到口语翻译服务的不足,本系统实现端到端的口语与手语双向翻译。通过嵌入手语识别模型,系统能够接受手语视频作为输入,并输出对应口语。
  • 通过构建手语知识库并集成手语纠错算法,系统实现手语辅助教学功能。

图2 手语翻译与辅助教学系统组成

系统主要功能均集成部署于云端,在移动端设备开放使用接口,用户能够轻易获取到本项目所提供各项的手语服务。

手语翻译

手语识别算法实现对连续输入的口语语音或文本与手语进行相互转换,主要在德语、英语场景下进行训练。

数据集介绍

我们使用PHOENIX-2014-T(德语,简称为ph14)和ASLG-PC12(英语,简称为aslg)两个数据集训练模型。数据集的语料统计分布情况如图3所示。


图3 手语翻译数据集介绍
模型框架

手语翻译模型的输入是自然文本,输出也是自然文本。图4直观地阐述手语翻译模型的执行流程,首先模型将输入文本分词为token(词元),然后再基于词向量预训练模型GLOVE对token进行向量化编码。在获得输入文本的编码后,采用Transformer架构实现Sequence-to-sequence模型,以自回归的方式生成翻译后的解码特征向量。最后基于softmax层解码对应的翻译句子的索引,拼接成最终的口语句子。


图4 手语翻译模型框架
主要挑战

这项工作的目标是将已识别的多个独立的手语序列(模型输入)转换为一个完整的口语句子(模型输出)。与一般的神经机器翻译相比,手语翻译主要面临着如下挑战:

  • 手语与口语表征空间的差异:手语的表示空间会明显小于口语句子,从而增加了网络学习的难度。
解决表征空间差异–多层级数据增强策略

除了现有数据集中的手语-口语对,我们还使用上采样方法作为我们的数据增强算法,并生成口语-口语对作为扩展样本,将口语信息引入到手语中。因而,扩大了手语的分布空间。从3个层级对采样规模进行约束:

  • 单词级别:单词差异率(Vocabulary Different Ratio,VDR),评估口语与手语单词级别上的差异。

以及稀疏词比例(Rare Vocabulary Ratio,RVR):


  • 句子级别:单词覆盖率(Sentence Cover Ratio,SCR)来评价句子对之间的相似度。

  • 数据集级别:采用数据集长度差异指标(Dataset Length-difference Ratio,DLR)来判断数据集层级上口语与手语的表示空间差异。

最后基于加权求和策略求得上采样的规模:


主要代码实现

定义模型初始化和数据增强函数:

import os
from tqdm import tqdm
import numpy as np

def data_preprocess(root_path):
    """
    数据预处理,统计数据集的单词数、以及文本分词
    """
    special_token=['<s>','<e>','<unk>']
    de_vocab=[]
    for file in tqdm(os.listdir(root_path)):
        file_path = os.path.join(root_path, file)
        with open(file_path, 'r') as f:
            for sentence in f.readlines():
                de_vocab.extend(sentence.strip().split(" "))
    de_vocab = list(set(de_vocab))
    print("vocab num: ", len(de_vocab))
    with open(os.path.join(root_path, "vocab.de"),'a+') as f:
        for item in special_token:
            f.write(item+'\n')
        for item in de_vocab:
            f.write(item+'\n')

def data_augmentation(gloss_file, text_file, sampling_ratio):
    """
    手语翻译 多层级数据增强算法
    """
    gloss_data = []
    # 读所有手语样本=》gloss_data
    with open(gloss_file, 'r') as f:
        gloss_sentence = [item.strip().lower() for item in f.read().split('\n')]
        for item in gloss_sentence:
            words = item.split()
            gloss_eff = []
            for word in words:
                if "__" not in word:
                    gloss_eff.append(word)
            gloss_data.append(" ".join(gloss_eff))
    # 读所有口语句子=》text_data
    with open(text_file, 'r', encoding='utf-8') as f:
        text_data = [item.strip() for item in f.read().split('\n')]

    gloss_data.pop(-1)
    # 设置采样ratio
    ratio = int(sampling_ratio * len(gloss_data))
    index = np.random.randint(low=0, high=len(gloss_data)-1, size=ratio)

    # 生成口语-口语对进行上采样
    for item in index:
        gloss_data.append(text_data[item][0:-2])
        text_data.append(text_data[item])

def model_definition(args):
    """
    训练准备:定义模型、损失函数、优化器等参数
    """
    # 定义transformer模型
    transformer = TransformerModel(
        src_vocab_size=args.src_vocab_size,
        trg_vocab_size=args.trg_vocab_size,
        max_length=args.max_length + 1,
        n_layer=args.n_layer,
        n_head=args.n_head,
        d_model=args.d_model,
        d_inner_hid=args.d_inner_hid,
        dropout=args.dropout,
        weight_sharing=args.weight_sharing,
        bos_id=args.bos_idx,
        eos_id=args.eos_idx)

    # 定义损失函数,这里输出为序列id的预测,所以采用交叉熵函数
    criterion = CrossEntropyCriterion(args.label_smooth_eps, args.bos_idx)

    # 定义学习率衰减策略
    scheduler = paddle.optimizer.lr.NoamDecay(
        args.d_model, args.warmup_steps, args.learning_rate, last_epoch=0)

    # 定义优化器
    optimizer = paddle.optimizer.Adam(
        learning_rate=scheduler,
        beta1=args.beta1,
        beta2=args.beta2,
        epsilon=float(args.eps),
        parameters=transformer.parameters())
    return transformer, criterion, scheduler, optimizer

训练过程如图5所示。


图5 手语翻译在飞桨平台的训练过程

运行如下代码加载模型参数并进行推理:

import paddle
import yaml
import time
from paddlenlp.data import Vocab, Pad
from paddlenlp.transformers import InferTransformerModel
import sys 
sys.path.append('/home/aistudio/external-libraries')
from attrdict import AttrDict

def prepare_infer(args):
    # 加载vocab类
    vocab = Vocab.load_vocabulary(args.trg_vocab_fpath, bos_token=args.special_token[0],
                                  eos_token=args.special_token[1],
                                  unk_token=args.special_token[2])
    padding_vocab = (
        lambda x: (x + args.pad_factor - 1) // args.pad_factor * args.pad_factor
    )
    args.src_vocab_size = padding_vocab(len(vocab))
    args.trg_vocab_size = padding_vocab(len(vocab))
    # 定义模型
    transformer = InferTransformerModel(
        src_vocab_size=args.src_vocab_size,
        trg_vocab_size=args.trg_vocab_size,
        max_length=args.max_length + 1,
        num_encoder_layers=args.n_layer,
        num_decoder_layers=args.n_layer,
        n_layer=args.n_layer,
        n_head=args.n_head,
        d_model=args.d_model,
        d_inner_hid=args.d_inner_hid,
        dropout=args.dropout,
        weight_sharing=args.weight_sharing,
        bos_id=args.bos_idx,
        eos_id=args.eos_idx,
        beam_size=args.beam_size,
        max_out_len=args.max_out_len)
    # 加载模型参数
    model_dict = paddle.load(args.inference_model_dir)
    transformer.load_dict(model_dict)
    transformer.eval()
    return transformer, vocab


def post_process_seq(seq, bos_idx, eos_idx, output_bos=False, output_eos=False):
    """
    后处理,从句号处截断
    """
    eos_pos = len(seq) - 1
    for i, idx in enumerate(seq):
        if idx == eos_idx:
            eos_pos = i
            break
    seq = [
        idx for idx in seq[:eos_pos + 1]
        if (output_bos or idx != bos_idx) and (output_eos or idx != eos_idx)
    ]
    return seq


def infer_spoken_sen(infer_input, infer_model, infer_vocab):
    """
    infer入口
    """
    infer_start = time.time()
    infer_input = infer_input.lower().split()
    tokens = infer_vocab.to_indices(infer_input)
    word_pad = Pad(args.bos_idx)
    tokens = word_pad([tokens+ [args.eos_idx]])
    id_lists = infer_model(src_word=paddle.to_tensor(tokens)).transpose([0, 2, 1]).numpy()
    seq = post_process_seq(id_lists[0][0], args.bos_idx, args.eos_idx)
    word_list = infer_vocab.to_tokens(seq)
    return " ".join(word_list), time.time()-infer_start


if __name__ == '__main__':
    # 定义infer基本参数
    yaml_file = 'model_param/infer.yaml'
    with open(yaml_file, 'rt') as f:
        args = AttrDict(yaml.safe_load(f))
    random_seed = eval(str(args.random_seed))
    if random_seed is not None:
        paddle.seed(random_seed)
    start_time = time.time()
    model, vocab = prepare_infer(args)
    index_num = 28
    print("-"*index_num, " 手语翻译 测试开始 ", "-"*index_num)
    predict_sen = "region koennen nebel"
    result_sen, infer_time = infer_spoken_sen(predict_sen, model, vocab)
    end_time = time.time()
    print(" · 您输入的句子为(手语): ", predict_sen)
    print(" · 模型翻译结果(口语): ", result_sen)
    print(" · 本次翻译用时: \t{:.3f}s.".format(infer_time))
    print(" · 测试总用时(+模型加载): \t{:.3f}s.".format(end_time-start_time))
    print("-"*index_num, " 手语翻译 测试完成 ", "-"*index_num)
----------------------------  手语翻译 测试开始  ----------------------------
 · 您输入的句子为(手语):  region koennen nebel
 · 模型翻译结果(口语):  in den niederungen bildet sich hier und da nebel .
 · 本次翻译用时: 	0.801s.
 · 测试总用时(+模型加载): 	2.227s.
----------------------------  手语翻译 测试完成  ----------------------------

手语识别

手语识别算法的主要功能是识别出手语视频中对应的手语动作,并按照其出现的顺序排列。项目采用容易获取手语动作信息的RGB摄像头作为输入设备。手语动作识别处理流程如图6所示,摄像头作为输入设备采集用户手语动作视频,将其转换为图片序列作为手语识别算法模型输入。模型采用基于注意力机制的编码器架构作为主体,其本质是将图片序列转换为手语文本序列的Sequence-to-Sequence模型。模型首先通过卷积网络提取图片特征并通过周期函数引入其位置和时序信息,然后由编码器学习图片序列中的手语信息和上下文语义关联特征。最后模型通过简单的线性层和softmax层激活得到手语单词。


图6 手语识别模型框架
主要代码实现

定义图片卷积层:

class SpatialEmbeddings(nn.Layer):

    """
    图片卷积层
    """
    def __init__(self,embedding_dim: int,input_size: int,):
        """
        网络初始化
        参数:
             embedding_dim: 图片特征隐层维度
             input_size: 图片特征嵌入维度
        """
        super().__init__()

        self.embedding_dim = embedding_dim
        self.input_size = input_size
        self.ln = nn.Linear(self.input_size, self.embedding_dim)
        self.norm = MaskedNorm(num_features=embedding_dim)
        self.activation = nn.Softsign()

    def forward(self, x: Tensor, mask: Tensor) -> Tensor:
        """
        前向传播
        参数:
            mask: 图片特征mask
            x: 输入图片特征
        返回:
            图片深度特征
        """

        x = self.ln(x)
        x = self.norm(x, mask)
        x = self.activation(x)

        return x


class MaskedNorm(nn.Layer):
    """
        对有mask的输入进行批归一化
        参考https://discuss.pytorch.org/t/batchnorm-for-different-sized-samples-in-batch/44251/8
    """

    def __init__(self,num_features):
        super().__init__()
        self.norm = nn.BatchNorm1D(num_features=num_features)
        self.num_features = num_features

    def forward(self, x: Tensor, mask: Tensor):
        if self.training:
            reshaped = x.reshape([-1, self.num_features])
            reshaped_mask = mask.reshape([-1, 1]) > 0
            selected = paddle.masked_select(reshaped, reshaped_mask.tile([1,reshaped.shape[1]])).reshape(
                [-1, self.num_features])
            batch_normed = self.norm(selected)
            scattered = reshaped
            scattered[reshaped_mask.reshape([-1])] = batch_normed
            return scattered.reshape([x.shape[0], -1, self.num_features])
        else:
            reshaped = x.reshape([-1, self.num_features])
            batched_normed = self.norm(reshaped)
            return batched_normed.reshape([x.shape[0], -1, self.num_features])

定义基于注意力的编码器层:

class TransformerEncoder(Encoder):
    """
    基于注意力机制的编码器层
    """
    def __init__(
        self,
        hidden_size: int = 512,
        ff_size: int = 2048,
        num_layers: int = 8,
        num_heads: int = 4,
        dropout: float = 0.1,
        emb_dropout: float = 0.1,
    ):
        """
        参数初始化
        参数:
            hidden_size: 隐层嵌入维度
            ff_size: 前向传播层维度
            num_layers: 基于注意力的卷积层数
            num_heads: 多头注意力头数
            dropout: dropout概率
            emb_dropout: 嵌入层dropout概率.
        """
        super(TransformerEncoder, self).__init__()

        # 构建注意力卷积层
        self.layers = nn.LayerList(
            [
                TransformerEncoderLayer(
                    size=hidden_size,
                    ff_size=ff_size,
                    num_heads=num_heads,
                    dropout=dropout,
                )
                for _ in range(num_layers)
            ]
        )

        self.layer_norm = nn.LayerNorm(hidden_size, epsilon=1e-6)
        self.pe = PositionalEncoding(hidden_size) # 位置嵌入
        self.emb_dropout = nn.Dropout(p=emb_dropout)

        self._output_size = hidden_size

    def forward(
        self, embed_src: Tensor, src_length: Tensor, mask: Tensor) -> (Tensor, Tensor):
        """
        前向传播
        参数:
            embed_src: 图片特征输入
            src_length: 输入长度
            mask: 指明mask区域
        返回:
            隐层状态
        """
        x = self.pe(embed_src)
        x = self.emb_dropout(x)

        for layer in self.layers:
            x = layer(x, mask)
        return self.layer_norm(x), None


class PositionalEncoding(nn.Layer):
    """
    位置嵌入编码
    """
    def __init__(self, size: int = 0, max_len: int = 5000):
        """
        初始化位置编码
        参数:
            max_len: 最大长度
            dropout: dropout概率
        """
        if size % 2 != 0:
            raise ValueError(
                "Cannot use sin/cos positional encoding with "
                "odd dim (got dim={:d})".format(size)
            )
        pe = paddle.zeros([max_len, size])
        position = paddle.arange(0, max_len).unsqueeze(1)
        div_term = paddle.exp(
            (paddle.arange(0, size, 2, dtype=paddle.float32) * -(math.log(10000.0) / size))
        )
        pe[:, 0::2] = paddle.sin(paddle.cast(position, 'float32') * div_term)
        pe[:, 1::2] = paddle.cos(paddle.cast(position, 'float32') * div_term)
        pe = pe.unsqueeze(0)  # shape: [1, size, max_len]
        super(PositionalEncoding, self).__init__()
        self.register_buffer("pe", pe)
        self.dim = size

    def forward(self, emb):
        """
        嵌入位置编码到输入中
        参数:
            输入图片特征
        返回:
            嵌入位置信息的图片特征
        """
        return emb + self.pe[:, : emb.shape[1]]



class TransformerEncoderLayer(nn.Layer):
    """
    基于注意力机制的卷积层
    """
    def __init__(
            self, size: int = 0, ff_size: int = 0, num_heads: int = 0, dropout: float = 0.1
    ):
        """
        卷积层初始化
        参数:
            ff_size: 隐层表示向量维度
            num_heads: 注意力头数
            dropout: dropout概率
        """
        super(TransformerEncoderLayer, self).__init__()

        self.layer_norm = nn.LayerNorm(size, epsilon=1e-6)
        self.src_src_att = MultiHeadedAttention(num_heads, size, dropout=dropout)
        self.feed_forward = PositionwiseFeedForward(input_size=size, ff_size=ff_size, dropout=dropout)
        self.dropout = nn.Dropout(dropout)
        self.size = size

    def forward(self, x: Tensor, mask: Tensor) -> Tensor:
        """
        前向传播, 之后进行自注意力特征提取, 然后使用dropout避免过拟合
        参数: 
            x: 输入
            mask: 输入掩码
        返回:
            隐层特征
        """
        x_norm = self.layer_norm(x) # 层归一化
        h = self.src_src_att(x_norm, x_norm, x_norm, mask) # 自注意力
        h = self.dropout(h) + x # 残差连接
        o = self.feed_forward(h)
        return o


class MultiHeadedAttention(nn.Layer):
    """
    多头注意力,参考论文"Attention is All You Need"
    """

    def __init__(self, num_heads: int, size: int, dropout: float = 0.1):
        super(MultiHeadedAttention, self).__init__()

        assert size % num_heads == 0
        self.head_size = head_size = size // num_heads
        self.model_size = size
        self.num_heads = num_heads

        self.k_layer = nn.Linear(size, num_heads * head_size)
        self.v_layer = nn.Linear(size, num_heads * head_size)
        self.q_layer = nn.Linear(size, num_heads * head_size)

        self.output_layer = nn.Linear(size, size)
        self.softmax = nn.Softmax(axis=-1)
        self.dropout = nn.Dropout(dropout)

    def forward(self, k: Tensor, v: Tensor, q: Tensor, mask: Tensor = None):
        batch_size = k.shape[0]
        num_heads = self.num_heads

        k = self.k_layer(k)
        v = self.v_layer(v)
        q = self.q_layer(q)

        k = paddle.transpose(k.reshape([batch_size, -1, num_heads, self.head_size]), [0, 2, 1, 3])
        v = paddle.transpose(v.reshape([batch_size, -1, num_heads, self.head_size]), [0, 2, 1, 3])
        q = paddle.transpose(q.reshape([batch_size, -1, num_heads, self.head_size]), [0, 2, 1, 3])

        q = q / math.sqrt(self.head_size)

        scores = paddle.matmul(q, k.transpose([0, 1, 3, 2]))

        if mask is not None:
            scores = masked_fill(scores, ~mask.unsqueeze(1), float("-inf"))

        attention = self.softmax(scores)
        attention = self.dropout(attention)

        context = paddle.matmul(attention, v)
        context = context.transpose([0, 2, 1, 3]).reshape([batch_size, -1, num_heads * self.head_size])

        output = self.output_layer(context)

        return output


class PositionwiseFeedForward(nn.Layer):
    """
    多层感知机
    """
    def __init__(self, input_size, ff_size, dropout=0.1):
        """
        参数:
            input_size: 输入维度
            ff_size: 隐层维度
            dropout: dropout概率
        """
        super(PositionwiseFeedForward, self).__init__()
        self.layer_norm = nn.LayerNorm(input_size, epsilon=1e-6)
        self.pwff_layer = nn.Sequential(
            nn.Linear(input_size, ff_size),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(ff_size, input_size),
            nn.Dropout(dropout))

    def forward(self, x):
        x_norm = self.layer_norm(x)
        return self.pwff_layer(x_norm) + x

模型定义:

class SignModel(nn.Layer):
    """
    模型基类
    """
    def __init__(self,encoder: Encoder,gloss_output_layer: nn.Layer,sgn_embed: SpatialEmbeddings,gls_vocab: GlossVocabulary):
        """
        模型初始化
        参数
            encoder: 编码器层
            gloss_output_layer: 输出层
            sgn_embed: 图片卷积层
            gls_vocab: 手语词典
        """
        super().__init__()
        self.encoder = encoder
        self.sgn_embed = sgn_embed
        self.gls_vocab = gls_vocab
        self.gloss_output_layer = gloss_output_layer

    def forward(self,sgn: Tensor,sgn_mask: Tensor,sgn_lengths: Tensor) -> (Tensor, Tensor, Tensor, Tensor):
        """
        模型前向传播
        参数:
            sgn: 手语输入
            sgn_mask: 输入掩码
            sgn_lengths: 输入长度
        返回:
            模型预测结果
        """
        encoder_output, encoder_hidden = self.encode(sgn=sgn, sgn_mask=sgn_mask, sgn_length=sgn_lengths)
        gloss_scores = self.gloss_output_layer(encoder_output)
        gloss_probabilities = nn.functional.log_softmax(gloss_scores,axis=2) # 激活层
        gloss_probabilities = gloss_probabilities.transpose([1, 0, 2])

        return gloss_probabilities

    def encode(self, sgn: Tensor, sgn_mask: Tensor, sgn_length: Tensor) -> (Tensor, Tensor):
        return self.encoder(embed_src=self.sgn_embed(x=sgn, mask=sgn_mask),src_length=sgn_length,mask=sgn_mask)

    def get_loss_for_batch(self,batch: Batch,recognition_loss_function: nn.Layer) -> Tensor:
        """
        损失计算
        参数:
            batch: 输入数据
            recognition_loss_function: 手语识别损失函数, 默认为CTC损失
            recognition_loss_weight: 损失权重
        返回:
            recognition_loss: 总损失大小
        """
        gloss_probabilities = self.forward(sgn=batch.sgn,sgn_mask=batch.sgn_mask,sgn_lengths=batch.sgn_lengths)

        assert gloss_probabilities is not None

        recognition_loss = (recognition_loss_function(
                            gloss_probabilities,
                            batch.gls.cast('int32'),
                            batch.sgn_lengths.cast('int64'),
                            batch.gls_lengths.cast('int64')))

        return recognition_loss

构建手语识别模型:

def build_model(
    sgn_dim: int,
    gls_vocab: GlossVocabulary
) -> SignModel:
    """
    构建手语识别模型
    参数:
        sgn_dim: 手语图片嵌入特征维度
        gls_vocab: sign gloss vocabulary
    返回:
        初始化完成的模型
    """
    sgn_embed: SpatialEmbeddings = SpatialEmbeddings(embedding_dim = 512,input_size=sgn_dim)
    encoder = TransformerEncoder(hidden_size = 512,ff_size=2048,num_layers = 3,num_heads = 8,dropout = 0.1,emb_dropout=0.1)
    gloss_output_layer = nn.Linear(encoder.output_size, len(gls_vocab))
    model: SignModel = SignModel(encoder=encoder,gloss_output_layer=gloss_output_layer,sgn_embed=sgn_embed,gls_vocab=gls_vocab)

    return model

训练损失函数如图7所示,更多训练和测试数据见项目VisualDL可视化日志。


图7 手语识别模型训练损失

手语教学

本系统第二个核心功能是基于手语翻译功能进行扩展,实现手语教学功能。手语教学旨在帮助听障用户完成手语的系统性巩固学习。如图8所示,教学模式分为以下两种:

  • 检索模式:用户输入想要学习的语句或单词,系统除了呈现手语翻译结果,还将在知识库中进行检索,给出关联知识,其中包括手语逻辑、单词知识、规范用例等,借此实现视频演示和知识详解结合的教学功能。
  • 练习模式:基于对用户手语学习历史的分析,给定口语句子作为题目。用户需要根据提示完成手语动作并上传视频至系统。系统将识别用户手语动作并转换为手语序列。如果出现手语错误或遗漏,则给出即时反馈用于纠正。

图8 手语教学中的练习模式与检索模式示例

手语练习模式基于手语纠错算法进行开发,图9展示了手语纠错算法示例,该算法基于字符串匹配实现,算法对手语识别结果和正确答案进行比对,将错词、漏词和语序逻辑等错误情况汇总并给出评分。


图9 手语纠错流程例
主要代码实现

下面代码展示了手语纠错算法示例:

# 手语教学,根据用户输入的视频识别出用户输入的手语动作序列,并进行打分
import time
import numpy as np


class InputChecker:
    def __init__(self):
        self.score = 0.0
        self.wrong_type = ["顺序错误", "词语缺失", "多余输入"]
        self.wrong_type_max = [30, 50, 20]

    def evaluate_user_input(self, gold_ans, input_text):
        """
        对每一个用户输入评估三种类型的得分情况
        """
        gold_ans = gold_ans.split(" ")
        input_text = input_text.split(" ")
        self.score = self.cover_tokens(gold_ans, input_text) # 初始化得分
        if self.score == 0:
            return self.score, "很遗憾,请再重新学习一下吧。"
        order_score, order_wrong_param = self.order_check(gold_ans, input_text) # 顺序错误
        token_score, token_miss_param = self.token_check(gold_ans, input_text) # 词语缺失
        extra_score, extra_input_param = self.extra_input_check(gold_ans, input_text) # 多余输入
        self.score -= (order_score + token_score + extra_score)
        info = {self.wrong_type[0]: order_wrong_param, self.wrong_type[1]: token_miss_param, 
                self.wrong_type[2]: extra_input_param}
        return self.score, info

    def cover_tokens(self, gold_ans, input_text):
        cover_num = sum([1 for item in gold_ans if item in input_text])
        if cover_num == 0:
            return 0.0
        else:
            return 100.0

    def order_check(self, gold_ans, input_text):
        """
        顺序错误检查
        """
        common_list = [item for item in gold_ans if item in input_text]
        if len(common_list) < 2:
            return 0, None
        else:
            order_shuffle_score = 0
            wrong_order_pairs = []
            for i in range(len(common_list)):
                for j in range(i+1, len(common_list)):
                    pos_gold = [gold_ans.index(common_list[i]), gold_ans.index(common_list[j])]
                    pos_ans = [input_text.index(common_list[i]), input_text.index(common_list[j])]
                    if (pos_gold[1] > pos_gold[0] and pos_ans[1] > pos_ans[0]) or (pos_gold[1] < pos_gold[0] and pos_ans[1] < pos_ans[0]):
                        continue
                    else:
                        wrong_order_pairs.append(common_list[i] + "_" + common_list[j])
                        order_shuffle_score += 10
            if len(wrong_order_pairs) > 0:
                return min(order_shuffle_score, self.wrong_type_max[0]), " | ".join(wrong_order_pairs)
            else:
                return 0, None

    def token_check(self, gold_ans, input_text):
        """
        词语缺失检查
        """
        miss_num = sum([1 for item in gold_ans if item not in input_text])
        miss_tokens = [item for item in gold_ans if item not in input_text]
        minus_score = 10 * miss_num
        if len(miss_tokens) > 0:
            return min(minus_score, self.wrong_type_max[1]), " | ".join(miss_tokens)
        else:
            return 0, None

    def extra_input_check(self, gold_ans, input_text):
        """
        多余输入检查
        """
        extra_num = sum([1 for item in input_text if item not in gold_ans])
        extra_tokens = [item for item in input_text if item not in gold_ans]
        minus_score = 10 * extra_num
        if len(extra_tokens) > 0:
            return min(minus_score, self.wrong_type_max[2]), " | ".join(extra_tokens)
        else:
            return 0, None


if __name__ == '__main__':
    # 定义infer基本参数
    user_input = ["凉爽 骑车", "天气 我 出门 不", "热 天气 我 不 出门", "温度 热 我 骑车", "天气 热 我 出门 不"]
    golden = "天气 热 我 出门 不"
    start_time = time.time()
    index_num = 23
    input_checker = InputChecker()
    print("-"*index_num, " 手语教学 测试开始 ", "-"*index_num)
    for i in range(len(user_input)):
        score, info = input_checker.evaluate_user_input(golden, user_input[i])
        print(" => 学习记录 {} :".format(str(i)))
        print(" · 用户输入: ", " / ".join(user_input[i].split(" ")))
        print(" · 正确答案: ", " / ".join(golden.split(" ")))
        print(" · 综合评分:  {:.1f}".format(score))
        if isinstance(info, str):
            print(" . ", info)
        else:
            wrong_type = [key for key, value in info.items() if value is not None]
            wrong_type_param = [value for key, value in info.items() if value is not None]
            if len(wrong_type) > 0:
                print(" · 错误类型: ", "|".join(wrong_type))
                print(" · 详细信息: ")
                for i, j in zip(wrong_type, wrong_type_param):
                    print(" \t\t {} : {}".format(i, j))
            else:
                print(" · 恭喜,完全正确!本关闯关成功!")
    print(" · 本次服务用时:\t{:.3f}s.".format(time.time()-start_time))
    print("-"*index_num, " 手语教学 测试完成 ", "-"*index_num)
-----------------------  手语教学 测试开始  -----------------------
 => 学习记录 0 :
 · 用户输入:  凉爽 / 骑车
 · 正确答案:  天气 / 热 / 我 / 出门 / 不
 · 综合评分:  0.0
 .  很遗憾,请再重新学习一下吧。
 => 学习记录 1 :
 · 用户输入:  天气 / 我 / 出门 / 不
 · 正确答案:  天气 / 热 / 我 / 出门 / 不
 · 综合评分:  90.0
 · 错误类型:  词语缺失
 · 详细信息: 
 		 词语缺失 : 热
 => 学习记录 2 :
 · 用户输入:  热 / 天气 / 我 / 不 / 出门
 · 正确答案:  天气 / 热 / 我 / 出门 / 不
 · 综合评分:  80.0
 · 错误类型:  顺序错误
 · 详细信息: 
 		 顺序错误 : 天气_热 | 出门_不
 => 学习记录 3 :
 · 用户输入:  温度 / 热 / 我 / 骑车
 · 正确答案:  天气 / 热 / 我 / 出门 / 不
 · 综合评分:  50.0
 · 错误类型:  词语缺失|多余输入
 · 详细信息: 
 		 词语缺失 : 天气 | 出门 | 不
 		 多余输入 : 温度 | 骑车
 => 学习记录 4 :
 · 用户输入:  天气 / 热 / 我 / 出门 / 不
 · 正确答案:  天气 / 热 / 我 / 出门 / 不
 · 综合评分:  100.0
 · 恭喜,完全正确!本关闯关成功!
 · 本次服务用时:	0.007s.
-----------------------  手语教学 测试完成  -----------------------

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值