Main Content

このページの内容は最新ではありません。最新版の英語を参照するには、ここをクリックします。

レーダー信号と深層学習を使用した手のジェスチャの分類

この例では、多入力単出力畳み込みニューラル ネットワーク (CNN) を使用して超広帯域 (UWB) インパルス レーダー信号データを分類する方法を示します。

はじめに

UWB インパルス レーダーなどのセンサーを使用して取得された動きベースの信号データには、さまざまなジェスチャに固有のパターンが含まれています。モーション データと動きを関連付けることには、作業面での利点がいくつかあります。たとえば、手のジェスチャ認識は、人間とコンピューターの非接触対話にとって重要です。この例は、深層学習ソリューションを使用して、手のジェスチャ データセット内のパターンからの特徴抽出を自動化し、すべての信号サンプルにラベル付けすることを目的としています。

UWB ジェスチャは、一般に公開されている動的手のジェスチャのデータセット [1] です。これには、8 人の異なる人間のボランティアから収集された合計 9,600 個のサンプルが含まれています。各記録を取得するために、検査員は実験設定の左側、上部、右側に個別の UWB インパルス レーダーを配置し、その結果として 3 つの受信レーダー信号データ行列を得ました。ボランティアは、12 の動的な手の動きで構成されるジェスチャ ボキャブラリに基づいて手のジェスチャを実行しました。

  1. 左-右スワイプ (L-R スワイプ)

  2. 右-左スワイプ (R-L スワイプ)

  3. 上-下スワイプ (U-D スワイプ)

  4. 下-上スワイプ (D-U スワイプ)

  5. 斜め-左-右-上-下スワイプ (Diag-LR-UD スワイプ)

  6. 斜め-左-右-下-上スワイプ (Diag-LR-DU スワイプ)

  7. 斜め-右-左-上-下スワイプ (Diag-RL-UD スワイプ)

  8. 斜め-右-左-下-上スワイプ (Diag-RL-DU スワイプ)

  9. 時計回りの回転

  10. 反時計回りの回転

  11. 内側への押し込み

  12. 空のジェスチャ

各手のジェスチャの動きは 3 つの独立した UWB インパルス レーダーによってキャプチャされるため、3 つの信号を個別の入力として受け入れる CNN アーキテクチャを使用します。CNN モデルは、各信号を組み合わせて最終的なジェスチャ ラベル予測を行う前に、各信号から特徴情報を抽出します。そのため、多入力単出力 CNN は、最小限に前処理されたレーダー信号データ行列を使用して、さまざまなジェスチャを分類します。

データのダウンロード

各レーダー信号データ行列は、生成元の手のジェスチャとしてラベル付けされます。8 人の異なる人間のボランティアが 12 の個別の手のジェスチャを実行し、合計 96 回の試行が 96 個の MAT ファイルに保存されました。各 MAT ファイルには、実験設定で使用された 3 つのレーダーに対応する 3 つのレーダー データ行列が含まれています。それらには、LeftTop、および Right という名前が付けられます。ファイルは次の場所で入手できます。

https://ssd.mathworks.com/supportfiles/SPT/data/uwb-gestures.zip

データ ファイルを MATLAB Examples ディレクトリにダウンロードします。

datasetZipFolder = matlab.internal.examples.downloadSupportFile("SPT/data","uwb-gestures.zip");
datasetFolder = erase(datasetZipFolder,".zip");
if ~exist(datasetFolder,"dir")
    downloadLocation = fileparts(datasetZipFolder);
    unzip(datasetZipFolder,downloadLocation);
end

pretrainedNetwork.mat という名前の MAT ファイルに保存されている事前学習済みネットワーク misoNet を含む別個のファイルをダウンロードするという選択肢もあります。次の場所で入手できます。

https://ssd.mathworks.com/supportfiles/SPT/data/uwb-gestures-network.zip

doTrainingfalse に設定すると、学習手順をスキップし、事前学習済みのネットワークを分類に使用できます。doTrainingfalse に設定した場合、事前学習済みのネットワークはこの例の後半でダウンロードされます。例の実行でネットワークに学習させる場合は、必ず doTrainingtrue に設定してください。

doTraining = true;

データの探索

ファイル内のデータにアクセスするための信号データストアを作成します。SignalVariableNames パラメーターを使用して、各ファイルから読み取りたい信号変数名を指定します。この例では、データセットが "uwb-gestures" フォルダー内の MATLAB Examples ディレクトリに保存されていることを前提としています。そうでない場合は、変数 datasetFolder のデータへのパスを変更します。

sds = signalDatastore(datasetFolder,...
                      "IncludeSubfolders",true,...
                      "SignalVariableNames",["Left","Top","Right"],...
                      "FileExtensions",".mat",...
                      "ReadOutputOrientation","row");

データストアは、左側、上部、右側のレーダーのレーダー信号行列をこの順序で含む 3 要素の cell 配列を返します。

preview(sds)
ans=1×3 cell array
    {9000×189 double}    {9000×189 double}    {9000×189 double}

各レーダー信号行列の行と列は、それぞれ手のジェスチャの継続時間 (低速) とレーダーからの手の距離 (高速) を表します。データ収集中に、検査官は被験者が特定の手のジェスチャを 450 秒間繰り返す様子を記録しました。これは 9000 (低速) 行に相当します。90 個の低速フレーム内に 1 つの完全なジェスチャ モーションがあります。したがって、各レーダー信号行列には 100 個の完全な手のジェスチャ モーション サンプルが含まれています。各 UWB レーダーの範囲は 1.2 メートルで、189 個の高速ビンに対応します。

slowTimeFrames = 90;
recordedTimePerSample = 4.5;
radarRange = 1.2;

手のジェスチャの動きを可視化するには、UWB レーダーの位置、ジェスチャ、およびジェスチャ サンプル (1 ~ 100) を指定します。

radarToPlot = 1;
gestureToPlot = "_G1_";
gestureSample = 50;
 

選択した手のジェスチャとレーダー位置のレーダー信号行列を取得します。

sdssubset = subset(sds,contains(sds.Files,gestureToPlot));
radarDataMatrix = read(sdssubset);
radarDataMatrix = radarDataMatrix{radarToPlot};

normalize を使用してジェスチャ信号データを 0 ~ 1 の範囲に変換し、imagesc を使用して手のジェスチャ モーション サンプルを可視化します。

normalizedRadarData = normalize(radarDataMatrix,2,"range",[0 1]);
imagesc([0 radarRange],...
        [0 recordedTimePerSample],...
        normalizedRadarData(slowTimeFrames*(gestureSample-1)+1:slowTimeFrames*gestureSample,:),...
        [0 1]);
set(gca,"YDir","normal")
title("Raw Signal")
xlabel("Distance of Hand from the Radar (m)")
ylabel("Duration of Hand Gesture (s)")

このように、動きのパターンを識別するのは困難です。

生の信号には、レーダーの範囲内に存在する身体部分やその他の静的物体からの環境反射が含まれています。これらの不要な反射は "クラッター" として知られており、指数移動平均を実行するパルス キャンセラーを使用して除去することができます。この演算の伝達関数は次のとおりです。

H(z)=1-z-11-αz-1

これにより、α は平均化の量 [2] を制御する値 0α1 となります。分子係数と分母係数をそれぞれ [1 -1] と [1 -0.9] に設定して filter を使用し、生の信号からクラッターを除去します。

clutterRemovedSignal = filter([1 -1],[1 -0.9],radarDataMatrix,[],1);

クラッターを除去した信号を可視化して違いを確認します。

normalizedClutterRemovedSignal = normalize(clutterRemovedSignal,2,"range",[0 1]);
imagesc([0 radarRange],...
        [0 recordedTimePerSample],...
        normalizedClutterRemovedSignal(slowTimeFrames*(gestureSample-1)+1:slowTimeFrames*gestureSample,:),...
        [0 1]);
set(gca,"YDir","normal")
title("Clutter-Removed Signal")
xlabel("Distance of Hand from the Radar (m)")
ylabel("Duration of Hand Gesture (s)")

動きのパターンがより見やすくなったことに注目してください。たとえば、左側のレーダーの視点から "左-右スワイプ" を可視化することを選択した場合、手のジェスチャが継続するにつれてレーダーからの手の距離が増加することがわかります。

学習用データの準備

MAT ファイル名には、各レーダー信号行列のラベルに対応するジェスチャ コード (G1、G2、...、G12) が含まれています。categorical 配列を使用して、これらのコードをジェスチャ ボキャブラリ内のラベルに変換します。

[~,filenames] = fileparts(sds.Files);
gestureLabels = extract(filenames,"G"+digitsPattern);
gestureCodes = ["G1","G2","G3","G4",...
                "G5","G6","G7","G8",...
                "G9","G10","G11","G12"];
gestureVocabulary = ["L-R swipe",       "R-L swipe",       "U-D swipe",       "D-U swipe",...
                     "Diag-LR-UD swipe","Diag-LR-DU swipe","Diag-RL-UD swipe","Diag-RL-DU swipe",...
                     "clockwise",       "counterclockwise","inward push",     "empty"];
gestureLabels = categorical(gestureLabels,gestureCodes,gestureVocabulary);

配列データストア内のラベルを収集します。

labelDs = arrayDatastore(gestureLabels,"OutputType","cell");

信号データストアと配列データストアを結合して、各レーダーからの信号データとカテゴリカル ラベルを含む単一のデータストアを取得します。結果のデータストアをシャッフルして、MAT ファイルでの格納順序をランダム化します。

allDataDs = combine(sds,labelDs);
allDataDs = shuffle(allDataDs);
preview(allDataDs)
ans=1×4 cell array
    {9000×189 double}    {9000×189 double}    {9000×189 double}    {[Diag-LR-UD swipe]}

変換関数を使用すると、データストアによって読み取られるデータに補助関数 processData を適用できます。processData は、上のセクションで説明した正規化とフィルター処理を実行して、データを標準化し、クラッターを除去します。さらに、レーダー信号行列を個別の手のジェスチャ モーション サンプルに分割します。

allDataDs = transform(allDataDs,@processData);
preview(allDataDs)
ans=8×4 cell array
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}
    {90×189 double}    {90×189 double}    {90×189 double}    {[Diag-LR-UD swipe]}

ニューラル ネットワークの学習は反復的です。データストアでは、反復のたびにファイルからデータを読み取り、そのデータを変換してからネットワークの係数を更新します。個々のサンプルからデータが読み取られるため、学習のために再シャッフルして別のデータストアに挿入される前に、そのデータをメモリに読み取る必要があります。

この学習データセットはメモリにすべて収まるため、Parallel Computing Toolbox が使用できる場合、データを並列で変換してから、変換したデータをワークスペースに集めることができます。UseParallel フラグを true に設定して readall を使用し、並列プールを利用してすべての信号データとラベルをワークスペースに読み取ります。データがコンピューターのメモリに収まる場合、データをワークスペースにインポートしておけば、データの読み取りと変換を 1 回のみ実行すれば済むため、学習が高速になります。データがメモリに収まらない場合、学習エポックごとに学習関数にデータストアを渡して変換を実行しなければならないことに注意してください。

allData = readall(allDataDs,"UseParallel",true);
Starting parallel pool (parpool) using the 'local' profile ...
Connected to the parallel pool (number of workers: 8).

ラベルは allData の最後の列として返されます。countlabels を使用して、データセット内のラベル値の割合を取得します。ジェスチャがデータセット全体でバランスよく表現されていることに注意してください。

countlabels(allData(:,4))
ans=12×3 table
         Label          Count    Percent
    ________________    _____    _______

    D-U swipe            793     8.2664 
    Diag-LR-DU swipe     800     8.3394 
    Diag-LR-UD swipe     800     8.3394 
    Diag-RL-DU swipe     800     8.3394 
    Diag-RL-UD swipe     800     8.3394 
    L-R swipe            800     8.3394 
    R-L swipe            800     8.3394 
    U-D swipe            800     8.3394 
    clockwise            800     8.3394 
    counterclockwise     800     8.3394 
    empty                800     8.3394 
    inward push          800     8.3394 

データを学習セットと検証セットにランダムに分割し、テスト データを後で使用できるようにしておきます。この例では、学習、検証、テストの分割はそれぞれ 70%、15%、15% になります。splitlabels を使用して、元のデータセットと同じラベルの比率を維持するように、データを学習セット、検証セット、およびテスト セットに分割します。3 つのセット間でデータをランダムにシャッフルするには、randomized オプションを指定します。

idxs = splitlabels(allData(:,4),[0.7 0.15],"randomized");
trainIdx = idxs{1}; valIdx = idxs{2}; testIdx = idxs{3};

インデックスをもう一度ランダム化することで、同じ試行からサンプルを選択することを避けます。メモリ内の学習データと検証データを配列データストアに保存して、多入力ネットワークの学習に使用できるようにします。

trainData = allData(trainIdx(randperm(length(trainIdx))),:);
valData = allData(valIdx(randperm(length(valIdx))),:);
trainDataDs = arrayDatastore(trainData,"OutputType","same");
valDataDs = arrayDatastore(valData,"OutputType","same");

学習用のネットワークの準備

学習前のネットワーク アーキテクチャを定義します。各手のジェスチャの動きは 3 つの独立した UWB インパルス レーダーによってキャプチャされるため、3 つの信号を個別の入力として受け入れる CNN アーキテクチャを使用します。この推奨多入力単出力 CNN に学習させた後に得られる結果は、レーダー データ行列の 90 x 189 x 3 スタックを入力とする代替の単入力単出力 CNN で得られる結果よりもかなり優れています。

repeatBranch は、3 つのレーダー データ信号行列に対して個別に実行する演算を含んでいます。CNN モデルは、各信号から抽出された特徴情報を組み合わせて、最終的なジェスチャ ラベル予測を行う必要があります。mainBranch は、3 つの repeatBranch 出力を連結し、ラベルを推定する演算を含んでいます。手のジェスチャ モーション サンプルを受け入れるために、サイズ 90 x 189 の imageInputLayer を指定します。入力数を 3 に設定して additionLayer を指定し、3 つの分岐の出力を収集してモデルの分類セクションにそれを渡します。手のジェスチャごとに 1 つずつ、出力サイズ 12 の fullyConnectedLayer を指定します。softmaxLayerclassificationLayer を追加して、推定ラベルを出力します。

repeatBranch = [
    imageInputLayer([90 189 1],"Normalization", "none")

    convolution2dLayer(3,8,"Padding",1)
    batchNormalizationLayer
    reluLayer   
    maxPooling2dLayer(2,"Stride",2)

    convolution2dLayer(3,16,"Padding",1)
    batchNormalizationLayer
    reluLayer
    maxPooling2dLayer(2,"Stride",2)

    convolution2dLayer(3,32,"Padding",1)
    batchNormalizationLayer
    reluLayer
    maxPooling2dLayer(2,"Stride",2)

    convolution2dLayer(3,64,"Padding",1)
    batchNormalizationLayer
    reluLayer
    maxPooling2dLayer(2,"Stride",2)];

mainBranch = [
    additionLayer(3)
    fullyConnectedLayer(12)
    softmaxLayer
    classificationLayer];

repeatBranch を 3 回、mainBranch を 1 回追加して、layerGraph を定義します。各 repeatBranch の最終 maxPooling2dLayer の出力を additionLayer の入力に接続します。

misoCNN = layerGraph();
misoCNN = addLayers(misoCNN, repeatBranch);
misoCNN = addLayers(misoCNN, repeatBranch);
misoCNN = addLayers(misoCNN, repeatBranch);
misoCNN = addLayers(misoCNN, mainBranch);
misoCNN = connectLayers(misoCNN, "maxpool_4", "addition/in1");
misoCNN = connectLayers(misoCNN, "maxpool_8", "addition/in2");
misoCNN = connectLayers(misoCNN, "maxpool_12", "addition/in3");

この多入力単出力 CNN を可視化します。

plot(misoCNN);

適切なネットワーク性能を確認する学習プロセスに対するオプションを選択します。学習中の検証に使用されるように、必ず valDataDsValidationData として指定してください。各パラメーターの説明については、trainingOptions (Deep Learning Toolbox) のドキュメンテーションを参照してください。

options = trainingOptions("adam", ...
    "InitialLearnRate",1e-3, ...
    "MaxEpochs",3,... 
    "MiniBatchSize",32, ...
    "ValidationData",valDataDs,...
    "ValidationFrequency",40,...
    "Verbose",false,...
    "Plots","training-progress");

ネットワークの学習

trainNetwork コマンドを使用して CNN に学習させます。

if doTraining == true
    misoNet = trainNetwork(trainDataDs,misoCNN,options);
else
    pretrainedNetworkZipFolder = matlab.internal.examples.downloadSupportFile("SPT","data/uwb-gestures-network.zip");
    pretrainedNetworkFolder = erase(pretrainedNetworkZipFolder,".zip");
    if ~exist(pretrainedNetworkFolder,"dir")
        downloadLocation = fileparts(pretrainedNetworkZipFolder);
        unzip(pretrainedNetworkZipFolder,downloadLocation);
    end
    load(fullfile(pretrainedNetworkFolder,"pretrainedNetwork.mat"),"misoNet");
end

テスト データの分類

学習済み CNN と classify コマンドを使用してテスト データを分類します。

testData = allData(testIdx,:);
testData = {cat(4,testData{:,1}),cat(4,testData{:,2}),cat(4,testData{:,3}),cat(1,testData{:,4})};
actualLabels = testData{4};
predictedLabels = classify(misoNet,testData{1},testData{2},testData{3});
accuracy = mean(predictedLabels==actualLabels);
fprintf("Accuracy on the test set is %0.2f%%",100*accuracy)
Accuracy on the test set is 96.04%

混同行列を使用して分類性能を可視化します。

confusionchart(predictedLabels,actualLabels);

最大の混乱は、"反時計回り""時計回り" の動きの間、および "内側への押し込み""空" の動きの間です。

ネットワークの予測の調査

各入力分岐の最終最大プーリング層からスコアを取得することで、各レーダーからのデータが最終的なネットワークの信頼性にどの程度寄与したかをより深く理解できます。補助関数 getActivationData は、ソフトマックス正規化スコア (クラス メンバーシップの確率) と、最も高い 3 つのスコアに対応するインデックスを返します。

gestureToPlot = "L-R swipe";
gestureToPlotIndices = find(matches(string(actualLabels),gestureToPlot));
gestureSelection = randsample(gestureToPlotIndices,1);
actualLabel = actualLabels(gestureSelection);
predictedLabel = predictedLabels(gestureSelection);
allLabels = categories(actualLabels);

[leftScores, leftClassIds] = getActivationData(misoNet,testData,gestureSelection,"maxpool_4");
[topScores, topClassIds] = getActivationData(misoNet,testData,gestureSelection,"maxpool_8");
[rightScores, rightClassIds] = getActivationData(misoNet,testData,gestureSelection,"maxpool_12");

各入力分岐の演算完了後に、補助関数 plotActivationData を使用して各レーダーからのデータを可視化し、最高 3 つのスコアに対応するラベルをオーバーレイします。

t = tiledlayout(1,3);
plotActivationData(testData, allLabels, leftScores, leftClassIds,...
    gestureSelection, [0 radarRange],[0 recordedTimePerSample], "Left");
plotActivationData(testData, allLabels, topScores, topClassIds,...
    gestureSelection, [0 radarRange],[0 recordedTimePerSample], "Top");
plotActivationData(testData, allLabels, rightScores, rightClassIds,...
    gestureSelection, [0 radarRange],[0 recordedTimePerSample], "Right");
title(t,["Actual Label : "+string(actualLabel);"Predicted Label : "+string(predictedLabel)],"FontSize",12);
xlabel(t,"Distance of Hand from the Radar (m)","FontSize",11)
ylabel(t,"Duration of Hand Gesture (s)","FontSize",11)

まとめ

この例では、多入力 CNN を使用して 3 つの独立したレーダー データ行列から情報を抽出し、手のジェスチャの動きを分類する方法を学習しました。多入力アーキテクチャにより、同じイベントを記録する複数のセンサーによって生成されたデータを利用することができました。

補助関数

function dataOut = processData(dataIn)
    label = dataIn(end);
    dataIn(end) = [];
    dataOut = cellfun(@(x) filter([1 -1],[1 -0.9],x,[],1),dataIn,"UniformOutput",false);
    dataOut = cellfun(@(x) normalize(x,2,"range",[0 1]),dataOut,"UniformOutput",false);
    dataOut = cat(1,dataOut{:});
    dataOut = mat2cell(dataOut,90*ones(1,size(dataOut,1)/90));
    dataOut = reshape(dataOut,[],3);
    dataOut(:,4) = label;
end

function [scores, classIds] = getActivationData(net, testData, index, layer)
    activation = activations(net,testData{1}(:,:,index),testData{2}(:,:,index),testData{3}(:,:,index),layer,"OutputAs","columns");
    fcWeights = net.Layers(end-2).Weights;
    fcBias = net.Layers(end-2).Bias;
    scores = fcWeights*activation + fcBias;
    scores = exp(scores)/sum(exp(scores));  
    [~,classIds] = maxk(scores,3);
end

function plotActivationData(testData, labels, scores, ids, sampleIdx, xlimits, ylimits, plotTitle)
    if plotTitle == "Left"
        gesture = 1;
    elseif plotTitle == "Top"
        gesture = 2;
    elseif plotTitle == "Right"
        gesture = 3;
    end
    nexttile;
    imagesc(xlimits,ylimits,testData{gesture}(:,:,sampleIdx),[0 1])
    text(0.3,4,plotTitle,"Color","red","FontWeight","bold","HorizontalAlignment","center")
    set(gca,"YDir","normal")
    title(string(labels(ids)) + ": " + string(round(scores(ids),3)),"FontSize",8);
end

参考文献

[1] Ahmed, S., Wang, D., Park, J. et al. UWB-gestures, a public dataset of dynamic hand gestures acquired using impulse radar sensors. Sci Data 8, 102 (2021). https://doi.org/10.1038/s41597-021-00876-0.

[2] Lazaro A, Girbau D, Villarino R. Techniques for clutter suppression in the presence of body movements during the detection of respiratory activity through UWB radars. Sensors (Basel, Switzerland). 2014 Feb;14(2):2595-2618. DOI: 10.3390/s140202595.

参考

| | | (Deep Learning Toolbox)