LSTM Autoencoder 문제
TLDR :
오토 인코더는 시계열 재구성을 과소 적합하고 평균값 만 예측합니다.
질문 설정 :
다음은 sequence-to-sequence 오토 인코더에 대한 나의 시도를 요약 한 것입니다. 이 이미지는이 문서에서 가져온 것입니다.https://arxiv.org/pdf/1607.00148.pdf

인코더 : 표준 LSTM 레이어. 입력 시퀀스는 최종 숨겨진 상태로 인코딩됩니다.
디코더 : LSTM Cell (제 생각에!). 마지막 요소부터 시작하여 한 번에 한 요소 씩 시퀀스를 재구성합니다 x[N]
.
길이 시퀀스에 대한 디코더 알고리즘은 다음과 같습니다 N
.
- 디코더 초기 숨김 상태 가져 오기
hs[N]
: 인코더 최종 숨김 상태를 사용합니다. - 시퀀스의 마지막 요소 재구성 :
x[N]= w.dot(hs[N]) + b
. - 다른 요소에 대해 동일한 패턴 :
x[i]= w.dot(hs[i]) + b
- 사용
x[i]
과hs[i]
같이에게 입력을 할LSTMCell
얻을x[i-1]
및hs[i-1]
최소 작동 예 :
다음은 인코더로 시작하는 내 구현입니다.
class SeqEncoderLSTM(nn.Module):
def __init__(self, n_features, latent_size):
super(SeqEncoderLSTM, self).__init__()
self.lstm = nn.LSTM(
n_features,
latent_size,
batch_first=True)
def forward(self, x):
_, hs = self.lstm(x)
return hs
디코더 클래스 :
class SeqDecoderLSTM(nn.Module):
def __init__(self, emb_size, n_features):
super(SeqDecoderLSTM, self).__init__()
self.cell = nn.LSTMCell(n_features, emb_size)
self.dense = nn.Linear(emb_size, n_features)
def forward(self, hs_0, seq_len):
x = torch.tensor([])
# Final hidden and cell state from encoder
hs_i, cs_i = hs_0
# reconstruct first element with encoder output
x_i = self.dense(hs_i)
x = torch.cat([x, x_i])
# reconstruct remaining elements
for i in range(1, seq_len):
hs_i, cs_i = self.cell(x_i, (hs_i, cs_i))
x_i = self.dense(hs_i)
x = torch.cat([x, x_i])
return x
두 가지를 하나로 모으기 :
class LSTMEncoderDecoder(nn.Module):
def __init__(self, n_features, emb_size):
super(LSTMEncoderDecoder, self).__init__()
self.n_features = n_features
self.hidden_size = emb_size
self.encoder = SeqEncoderLSTM(n_features, emb_size)
self.decoder = SeqDecoderLSTM(emb_size, n_features)
def forward(self, x):
seq_len = x.shape[1]
hs = self.encoder(x)
hs = tuple([h.squeeze(0) for h in hs])
out = self.decoder(hs, seq_len)
return out.unsqueeze(0)
그리고 여기 내 훈련 기능이 있습니다.
def train_encoder(model, epochs, trainload, testload=None, criterion=nn.MSELoss(), optimizer=optim.Adam, lr=1e-6, reverse=False):
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Training model on {device}')
model = model.to(device)
opt = optimizer(model.parameters(), lr)
train_loss = []
valid_loss = []
for e in tqdm(range(epochs)):
running_tl = 0
running_vl = 0
for x in trainload:
x = x.to(device).float()
opt.zero_grad()
x_hat = model(x)
if reverse:
x = torch.flip(x, [1])
loss = criterion(x_hat, x)
loss.backward()
opt.step()
running_tl += loss.item()
if testload is not None:
model.eval()
with torch.no_grad():
for x in testload:
x = x.to(device).float()
loss = criterion(model(x), x)
running_vl += loss.item()
valid_loss.append(running_vl / len(testload))
model.train()
train_loss.append(running_tl / len(trainload))
return train_loss, valid_loss
데이터:
뉴스에서 스크랩 한 대규모 이벤트 데이터 세트 (ICEWS). 각 이벤트를 설명하는 다양한 범주가 있습니다. 처음에는 이러한 변수를 원-핫 인코딩하여 데이터를 274 차원으로 확장했습니다. 그러나 모델을 디버그하기 위해 14 개의 타임 스텝 길이이고 5 개의 변수 만 포함하는 단일 시퀀스로 축소했습니다. 다음은 과적 합하려는 순서입니다.
tensor([[0.5122, 0.0360, 0.7027, 0.0721, 0.1892],
[0.5177, 0.0833, 0.6574, 0.1204, 0.1389],
[0.4643, 0.0364, 0.6242, 0.1576, 0.1818],
[0.4375, 0.0133, 0.5733, 0.1867, 0.2267],
[0.4838, 0.0625, 0.6042, 0.1771, 0.1562],
[0.4804, 0.0175, 0.6798, 0.1053, 0.1974],
[0.5030, 0.0445, 0.6712, 0.1438, 0.1404],
[0.4987, 0.0490, 0.6699, 0.1536, 0.1275],
[0.4898, 0.0388, 0.6704, 0.1330, 0.1579],
[0.4711, 0.0390, 0.5877, 0.1532, 0.2201],
[0.4627, 0.0484, 0.5269, 0.1882, 0.2366],
[0.5043, 0.0807, 0.6646, 0.1429, 0.1118],
[0.4852, 0.0606, 0.6364, 0.1515, 0.1515],
[0.5279, 0.0629, 0.6886, 0.1514, 0.0971]], dtype=torch.float64)
다음은 사용자 정의 Dataset
클래스입니다.
class TimeseriesDataSet(Dataset):
def __init__(self, data, window, n_features, overlap=0):
super().__init__()
if isinstance(data, (np.ndarray)):
data = torch.tensor(data)
elif isinstance(data, (pd.Series, pd.DataFrame)):
data = torch.tensor(data.copy().to_numpy())
else:
raise TypeError(f"Data should be ndarray, series or dataframe. Found {type(data)}.")
self.n_features = n_features
self.seqs = torch.split(data, window)
def __len__(self):
return len(self.seqs)
def __getitem__(self, idx):
try:
return self.seqs[idx].view(-1, self.n_features)
except TypeError:
raise TypeError("Dataset only accepts integer index/slices, not lists/arrays.")
문제:
모델은 내가 모델을 얼마나 복잡하게 만들었 든 아니면 지금 훈련하는 데 오래 되었든 상관없이 평균 만 학습합니다.
예측 / 재건 :

실제 :

내 연구 :
이 문제는이 질문에서 논의 된 문제와 동일합니다. LSTM 자동 인코더는 항상 입력 시퀀스의 평균을 반환합니다.
이 경우의 문제는 목적 함수가 손실을 계산하기 전에 목표 시계열을 평균화한다는 것입니다. 이것은 저자가 목적 함수에 적절한 크기의 입력을 가지고 있지 않았기 때문에 일부 방송 오류 때문이었습니다.
제 경우에는 이것이 문제라고 생각하지 않습니다. 모든 치수 / 크기가 정렬되어 있는지 확인하고 다시 확인했습니다. 나는 헤매고있다.
내가 시도한 다른 것들
- 7 개의 시간 단계에서 100 개의 시간 단계까지 다양한 시퀀스 길이로 이것을 시도했습니다.
- 시계열에서 다양한 수의 변수로 시도했습니다. 데이터에 포함 된 모든 274 개의 변수에 대해 일 변량으로 시도했습니다.
- 모듈
reduction
에서 다양한 매개 변수를 사용해 보았습니다nn.MSELoss
. 신문은을 요구sum
하지만 나는sum
및mean
. 차이 없음. - 이 논문에서는 순서를 역순으로 재구성해야합니다 (위의 그래픽 참조). 나는
flipud
원래 입력 (훈련 후 손실을 계산하기 전에)을 사용 하여이 방법을 시도했습니다 . 이것은 차이가 없습니다. - 인코더에 LSTM 레이어를 추가하여 모델을 더 복잡하게 만들려고했습니다.
- 나는 잠재 공간을 가지고 놀아 보았습니다. 입력 된 기능 수의 50 %에서 150 %까지 시도했습니다.
- 단일 시퀀스 ( 위 의 데이터 섹션에 제공됨)를 과적 합해 보았습니다 .
질문:
내 모델이 평균을 예측하게하는 원인은 무엇이며 어떻게 수정합니까?
답변
좋아요, 디버깅 후 이유를 알 것 같습니다.
TLDR
- 현재 시간 단계와 이전 시간 단계의 차이 대신 다음 시간 단계 값을 예측하려고합니다.
- 귀하의
hidden_features
수는 단 한 번의 샘플을 맞게 모델이없는 만드는 너무 작
분석
사용 된 코드
코드부터 시작하겠습니다 (모델이 동일 함) :
import seaborn as sns
import matplotlib.pyplot as plt
def get_data(subtract: bool = False):
# (1, 14, 5)
input_tensor = torch.tensor(
[
[0.5122, 0.0360, 0.7027, 0.0721, 0.1892],
[0.5177, 0.0833, 0.6574, 0.1204, 0.1389],
[0.4643, 0.0364, 0.6242, 0.1576, 0.1818],
[0.4375, 0.0133, 0.5733, 0.1867, 0.2267],
[0.4838, 0.0625, 0.6042, 0.1771, 0.1562],
[0.4804, 0.0175, 0.6798, 0.1053, 0.1974],
[0.5030, 0.0445, 0.6712, 0.1438, 0.1404],
[0.4987, 0.0490, 0.6699, 0.1536, 0.1275],
[0.4898, 0.0388, 0.6704, 0.1330, 0.1579],
[0.4711, 0.0390, 0.5877, 0.1532, 0.2201],
[0.4627, 0.0484, 0.5269, 0.1882, 0.2366],
[0.5043, 0.0807, 0.6646, 0.1429, 0.1118],
[0.4852, 0.0606, 0.6364, 0.1515, 0.1515],
[0.5279, 0.0629, 0.6886, 0.1514, 0.0971],
]
).unsqueeze(0)
if subtract:
initial_values = input_tensor[:, 0, :]
input_tensor -= torch.roll(input_tensor, 1, 1)
input_tensor[:, 0, :] = initial_values
return input_tensor
if __name__ == "__main__":
torch.manual_seed(0)
HIDDEN_SIZE = 10
SUBTRACT = False
input_tensor = get_data(SUBTRACT)
model = LSTMEncoderDecoder(input_tensor.shape[-1], HIDDEN_SIZE)
optimizer = torch.optim.Adam(model.parameters())
criterion = torch.nn.MSELoss()
for i in range(1000):
outputs = model(input_tensor)
loss = criterion(outputs, input_tensor)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(f"{i}: {loss}")
if loss < 1e-4:
break
# Plotting
sns.lineplot(data=outputs.detach().numpy().squeeze())
sns.lineplot(data=input_tensor.detach().numpy().squeeze())
plt.show()
기능 :
get_data
현재 시간 단계에서 이전 시간 단계의 값을 뺀subtract=False
경우 제공된 데이터에서 작동합니다.subtract=True
- 코드의 나머지 부분은
1e-4
손실에 도달 할 때까지 모델을 최적화합니다 (따라서 모델의 용량과 증가가 어떻게 도움이되는지, 시간 단계 대신 시간 단계의 차이를 사용할 때 어떤 일이 발생하는지 비교할 수 있음).
우리는 변수 HIDDEN_SIZE
와 SUBTRACT
매개 변수 만 다를 것입니다 !
빼기 없음, 작은 모델
HIDDEN_SIZE=5
SUBTRACT=False
이 경우 우리는 직선을 얻습니다. 모델이 데이터에 제시된 현상 (따라서 당신이 언급 한 평평한 선)을 맞추고 파악할 수 없습니다.

1,000 회 반복 제한에 도달했습니다.
빼기, 작은 모델
HIDDEN_SIZE=5
SUBTRACT=True
목표물은 이제 평평한 선에서 멀리 떨어져 있지만 너무 작은 용량으로 인해 모델을 맞출 수 없습니다.

1,000 회 반복 제한에 도달했습니다.
빼기 없음, 더 큰 모델
HIDDEN_SIZE=100
SUBTRACT=False
훨씬 나아졌고 우리의 목표는 942
단계를 거쳐 맞았습니다 . 더 이상 평평한 선이 없으며 모델 용량이 꽤 괜찮은 것 같습니다 (이 단일 예!)

빼기, 더 큰 모델
HIDDEN_SIZE=100
SUBTRACT=True
그래프가 그다지 예쁘게 보이지는 않지만 215
반복 만 수행 하면 원하는 손실에 도달했습니다 .

드디어
- 일반적으로 시간 단계 대신 시간 단계의 차이를 사용합니다 (또는 다른 변환에 대한 자세한 내용은 여기 참조 ). 다른 경우에는 신경망이 단순히 이전 단계의 출력을 복사하려고 시도합니다 (가장 쉬운 방법이므로). 이러한 방식으로 일부 최소값을 찾을 수 있으며이를 벗어나려면 더 많은 용량이 필요합니다.
- 타임 스텝 간의 차이를 사용할 때 이전 타임 스텝의 추세를 "외삽"할 방법이 없습니다. 신경망은 함수가 실제로 어떻게 변하는 지 배워야합니다.
- 더 큰 모델을 사용하십시오 (전체 데이터 세트에 대해 제
300
생각 과 같은 것을 시도해야 함 ).하지만 간단히 조정할 수 있습니다. - 사용하지 마십시오
flipud
. 양방향 LSTM을 사용하면 LSTM의 순방향 및 역방향 전달에서 정보를 얻을 수 있습니다 (역전 파와 혼동하지 마십시오!). 이것은 또한 당신의 점수를 높여야합니다
질문
좋아요, 질문 1 : 시계열의 변수 x에 대해 x [i]의 값이 아닌 x [i]-x [i-1]을 학습하도록 모델을 훈련시켜야한다고 말씀하시는 건가요? 내가 올바르게 통역하고 있습니까?
네, 맞습니다. 차이는 신경망이 과거 시간 단계를 너무 많이 기반으로 예측하려는 충동을 제거합니다 (단순히 마지막 값을 가져 와서 조금 변경함으로써).
질문 2 : 제로 병목 현상이 잘못되었다고 말씀하셨습니다. 그러나 예를 들어 자동 인코더로 간단한 고밀도 네트워크를 사용한다고 가정 해 보겠습니다. 올바른 병목을 얻는 것은 실제로 데이터에 달려 있습니다. 그러나 병목 현상을 입력과 같은 크기로 만들면 식별 기능을 얻을 수 있습니다.
예, 일을 더 어렵게 만드는 비선형 성이 없다고 가정합니다 ( 비슷한 경우는 여기 참조 ). LSTM의 경우 비선형이 있습니다.
또 다른 하나는 우리가 timesteps
단일 인코더 상태로 축적 되고 있다는 것 입니다. 그래서 본질적으로 우리는 timesteps
가능성이 거의없는 하나의 은닉 및 셀 상태로 정체성 을 축적해야합니다 .
마지막으로, 시퀀스의 길이에 따라 LSTM은 관련성이 가장 낮은 정보 중 일부를 잊어 버리기 쉬우므로 (모든 것을 기억할뿐만 아니라 그렇게하도록 설계된 것입니다) 따라서 가능성이 더 낮습니다.
num_features * num_timesteps는 입력과 같은 크기의 병목이 아니므로 모델이 ID를 학습하는 데 도움이되지 않아야합니까?
하지만 num_timesteps
각 데이터 포인트에 대해 가지고 있다고 가정합니다. 드물게 여기에있을 수 있습니다. 정체성과 네트워크의 비선형 성과 관련이없는 이유에 대해서는 위에서 답변했습니다.
ID 기능에 대한 마지막 요점입니다. 실제로 배우기 쉬웠다면 ResNet
아키텍처는 성공하지 못할 것입니다. 네트워크는 ID로 수렴하여 출력이 없으면 출력에 "작은 수정"을 할 수 있습니다.
"항상 타임 스텝 대신 타임 스텝의 차이를 사용합니다"라는 말이 궁금합니다. 모든 기능을 더 가깝게 가져 와서 정규화 효과가있는 것 같지만 이것이 왜 핵심인지 이해하지 못합니다. 더 큰 모델을 갖는 것이 해결책 인 것처럼 보였고 빼는 것이 도움이됩니다.
여기서 핵심은 실제로 모델 용량을 늘리는 것입니다. 빼기 트릭은 실제로 데이터에 따라 다릅니다. 극단적 인 상황을 상상해 봅시다.
- 우리는이
100
시간 단계를 하나의 기능 - 초기 시간 단계 값은
10000
- 다른 시간 단계 값은
1
기껏해야
신경망은 무엇을할까요 (여기서 가장 쉬운 것은 무엇입니까)? 아마도이 1
또는 작은 변경 사항을 노이즈로 버리고 1000
모두 (특히 일부 정규화가있는 경우) 만 예측할 수 1/1000
있습니다.
빼면 어떨까요? 전체 신경망 손실은 [0, 1]
대신 각 시간 단계 에 대한 한계에 [0, 1001]
있으므로 틀린 것이 더 심각합니다.
그리고 예, 그것은 어떤 의미에서 정규화와 관련이 있습니다.