入力データ

(batch_size, timesteps, features) が入力になる。batch_size はこのバッチ(学習・予測の1単位)中のサンプル数 (予測するデータの数) で stateless なら None (可変長) にできる、timesteps は与える時系列の長さ、features は特徴量。

features は 例えば1次元の波形データであれば 1 になる。3つのセンサーデータがある場合なら3になる。

timesteps は固定長にもできるし、可変長にもできる。可変長の場合は None を指定する。可変長といっても、単一バッチ中の timesteps の数は揃える必要がある。

timesteps を変えても RNN レイヤーのパラメータ数は変化しない。パラメータ数は features のサイズとRNNのユニット数に依存する。

timesteps とバッチ

stateless RNN の場合、RNN の内部状態はバッチごとにリセットされる。

1つのサンプルの出力は、与えた timesteps の数からしか影響されない。また、バッチ内の各サンプルは独立している。

例えば以下のような (3, 5, 1) なバッチを与えた場合、stateless RNN は、1つ目のサンプルに関しては [1, 2, 3, 4, 5] という情報をつかって 6 を予測するようになる。2つ目のサンプルも同様で、1つ目のサンプルとは内部状態が独立して (学習した重みはもちろん共有しているが) [ 2, 3, 4, 5, 6] から 7 を予測する。

# batch_input_shape = (3, 5, 1)
1 2 3 4 5 -> 6
2 3 4 5 6 -> 7
3 4 5 6 7 -> 8

stateful RNN

stateful の場合は手動でリセットしない限り、バッチの最後の内部状態はリセットされない。

極端な例だと1回のバッチで timesteps が1というこもありうる。混乱しやすいのでサンプル数1で例を示してみる。

以下のように3回のバッチにわけてstateful RNNへ入力を与える。すべてサンプル数 (batch_size) は固定。timesteps の数は任意。stateless ではリセットしていたバッチ最後の各サンプルの最後の状態を保持しているので、batch #2 では、1つのtimestepsを与えているだけだが、それまでの timesteps である 1 2 3 4 5 も考慮された状態で予測される。

# batch #1 shape = (1, 5, 1)
1 2 3 4 5 -> 6

# batch #2 shape = (1, 1, 1)
6 -> 7

# batch #3 shape = (1, 2, 1)
7, 8-> 9

stateless to stateful

statelessで学習させて stateful に予測させるということもできる。これらの違いは内部状態をバッチ間で共有するかどうかだけなので、十分に長い系列で学習しているならあまり問題はない。この場合は、学習時に与えたステップ数しか考慮されていないが、連続で推論して新しい系列を得たいときはstateless より早く予測できる (途中の状態を保持したままなため再度過去の時系列を与える必要がない)。

stateful で学習させるのは結構めんどうくさいので、stateless で十分に長い時系列を与えて学習させて、リアルタイム予測などで実際に使うときは stateful にするというやりかたはありかもしれない。

stateless to statefulの例

サンプルデータ

意味がある予測ではないけど、試しにやってみた。

space = np.linspace(0, 1, 1000)
data = signal.square(2 * np.pi * 50 *  space) * signal.square(2 * np.pi * 30 *  space) / 2 + 0.5

こういう一時データをRNN(GRU)で学習させてみる。急激な値の変化があると学習結果が面白いことが多いので矩形波にしてる。

stateless で学習させる

def create_model(batch_size=None, timesteps=None, stateful=False):
    inputs = Input(batch_shape=(batch_size, None, 1))
    x = inputs
    x = GRU(32, stateful=stateful, return_sequences=False)(x)
    x = Dense(1, activation='sigmoid')(x)
    outputs = x
    model = Model(inputs, outputs)
    model.compile(optimizer=keras.optimizers.Adam(lr=0.01), loss='binary_crossentropy', metrics=['accuracy'])
    return model

model = create_model(timesteps = 100)
model.summary()

gen = keras.preprocessing.sequence.TimeseriesGenerator(data[:-10].reshape( (len(data)-10), 1 ), data[10:], length=100)
model.fit_generator(gen, epochs=30, validation_data=gen, shuffle=False)

過去の状態から10ステップ先を1つ予測するという問題設定にした (特に問題に意味はない)

stateless でも stateful でも同じモデルを作れるようにして、まず stateless で学習させる。ここでは過去の系列は100ステップで学習させている。特に意味がある学習ではないので validation に同じ系列を指定してる。

stateless で予測させる

tstart = time.time()
predict_len = 500

start = 207
result = []
for n in range(predict_len):
    predicted = model.predict( data[start+n-100:start+n].reshape( (1, 100, 1) ) )
    result.append(predicted[0])

result = np.array( result)

elapsed = time.time() - tstart
print('predict {}ms'.format(elapsed * 1000))

plt.figure(figsize=(10,10))
plt.subplot(211)
plt.title('expected')
plt.plot(data[start+10:start+500+10])
plt.plot(result)
plt.subplot(212)
plt.title('predicted (stateless)')
plt.plot(result)
plt.show()

result_stateless = result

後々、stateful とのコードと合わせるため一括で predict せずサンプル数1つで予測させてる。

一応一通り予測できている。ちなみに "predict 6278.290271759033ms" かかった。

statefulモデルにする

model.save_weights('/tmp/param.hdf5')
model_stateful = create_model(batch_size = 1, stateful=True)
model_stateful.load_weights('/tmp/param.hdf5')
model_stateful.summary()

weight を保存して、stateful=True にしたモデルに作りなおして weight をロードする。つまり stateless のモデルと weight は一緒の状態になる。

stateful モデルで予測する

tstart = time.time()

start = 207
result = []
## preload same state with stateless
# model_stateful.predict( data[start-1-100:start-1].reshape( (1, 100, 1) ) )
for n in range(predict_len):
    # give just new 1 timestep
    predicted = model_stateful.predict( data[start+n-1:start+n].reshape( (1, 1, 1) ) )
    result.append(predicted[0])
    
result = np.array( result)

elapsed = time.time() - tstart
print('predict {}ms'.format(elapsed * 1000))

plt.figure(figsize=(10,10))
plt.subplot(311)
plt.title('predicted (stateless)')
plt.plot(result_stateless)
plt.subplot(312)
plt.title('predicted (stateful)')
plt.plot(result)
plt.subplot(313)
plt.title('predicted (stateful+stateless)')
plt.plot(result_stateless)
plt.plot(result)
plt.show()

stateful では毎回1ステップだけ与えて予測させる。最初は過去の時系列がないため乱れているけど、ステップを与え続けると stateless と同じように予測できる。stateful でもコメンアウトしたところを実行すれば最初から stateless と全く同じ状態になる。

これは "predict 327.3053169250488ms" で終わる。

  1. トップ
  2. tech
  3. RNN/LSTM/GRU の入力と stateful 化 (keras)

CW の最小単位である短点の長さ t は以下で求められる。w は符号速度、単位 wpm (通常は 10〜40wpm) 。

5 (トトトトト 短点5つ)や訂正信号 <HH> (トトトトトトトト 短点8つ) を送信しているときに最大の帯域幅になる。短点の長さ t の on/off の繰り返しであるので、波長 2t の矩形波となる。24wpm では t = 50ms なので波長100ms、すなわち 10Hz の矩形波。

これを搬送波に乗せると (AM変調なので) 両側波帯に帯域が広がるため最低でも 20Hz の帯域幅になる。矩波形なので奇数次数の高調波も発生し、5倍まで考慮するだけで100Hzになる。

なお10wpm で 4Hz、50wpm で 21Hz の矩形波。

コンスタレーション(信号空間ダイヤグラム)

普通CWのコンスタレーションを気にすることはないと思うが、一応確認しておくと、BPSK などと比べるときに想像しやすい (なぜ BPSK が CW/OOK と比べて 3dB 有利かとか)

上の図のように中央部 (off) と周辺部 (on)にわかれる。

これがたとえば BPSK の場合は、中央ではなく、左端と右端になる。すなわち信号空間的には距離はCWの2倍になる。2倍=3dBよくなるということはこういうこと

  1. トップ
  2. tech
  3. CW の信号帯域とコンスタレーション

RNN モールスデコーダの試作 | tech - 氾濫原

波形ではなくSFFTの結果を認識させる

モールスで必要なのはキャリア周波数の周辺帯域だけ。モールスは通信速度があまり早くなく、帯域幅も100Hz程度のため、周波数分解能と時間分解能にトレードオフのあるSFFTでも、十分解析可能なはずだと思った。

訓練データ

例によってここは node.js で作った。web-audio-engine を使って実際にモールスの信号をつくり、それを AnalyserNode で連続で FFT して画像をつくった。

ラベルデータは考えられるだけでいくつか作りかたがある

  1. 符号のon/off の正解データ (binary)
  2. どこからどこまでが、どの符号かのID (categorical)
  3. その符号を人間が認識できる最短の位置での符号 ID (categorical)

画像として処理するなら、どこからどこまでが符号でその符号が何かがわかればいいが、連続信号として処理したいなら、人間が認識できる最短の位置にラベル付けするのが正しそう、と考えた。

一応どの正解データも作れるのようにデータ生成コードを書いた。

符号のon/offはその後なんらかのアルゴリズムでさらにデコードする必要があるが、モールスの場合はクロックが固定ではないので割と面倒くさい。

ということで早々に符号列のパターンを直接認識させる方法をとることにした。認識させるのはあくまで符号列 (トツーやツートトト) であって、(A や B という文字ではない)

単語単位で認識させることができればもっと精度が上げられそうだけど、コールサイン (パターンはあるがほぼランダム) をとれないと意味がないので、ランダム精度を重視している。

備考

モールスは、人間の場合、速度によってやや異る方法で認識している。

  • 10wpm〜15wpm (低速) 「符号」をまとまって捉えられないので「長」「短」の組合せでデコードしている。符号表さえ覚えていればデコード可能。
  • 15〜30wpm (標準) ひとつの「符号」をまとめて捉えて直接文字で認識する (トツーときたら「A」と学習している) 音素の認識と似てる。聴覚受信の回路がある程度脳内でできていないと難しい。
  • 25〜40wpm (高速) ひとつの「単語」をまとめて捉えて文字列として認識する(ツー ト トトト ツー / TEST や ES / ト トトト) 。特に「E」は「ト」でとても短い符号なので、符号単位に認識していると間に合わない。

モデル

入力はタイムシリーズ型式 (None, timestep, features) timestep は 72、features はデコード対象周波数を中心とした magnitude。

出力はどのモールスの符号か?を表わす64次元のone-hotベクトル。

いろいろなモデルをつくっては壊して試した。

が、どうもこれではうまくいかなそうだという気がしてきた。2dB (ノイズ帯域500Hz) 程度のSNRでもほぼ認識できない。

  1. トップ
  2. tech
  3. モールスデコーダの続き