더북(TheBook)

2.3.5 데이터 정규화

보스턴 주택 데이터셋의 특성을 살펴보면 값의 범위가 다릅니다. NOX 범위는 0.4와 0.9 사이이고 TAX의 범위는 180에서 711 사이입니다. 옵티마이저는 특성과 가중치를 곱하여 더한 값이 주택 가격과 비슷하게 되도록 각 특성의 가중치를 찾는 식으로 선형 회귀 모델을 훈련합니다. 옵티마이저가 가중치 공간에서 그레이디언트를 따라 이런 가중치를 찾아다닌다는 것을 기억하세요. 일부 특성이 다른 특성과 스케일이 매우 다르면 특정 가중치가 다른 가중치보다 훨씬 더 민감할 것입니다. 따라서 한 방향으로 조금만 움직여도 다른 방향보다 출력이 훨씬 더 많이 변하게 됩니다. 이로 인해 불안정해지고 모델 훈련이 어려워집니다.

이에 대응하기 위해 데이터를 정규화(normalization)하겠습니다. 특성의 평균이 0이고 단위 표준 편차를 가지도록 특성을 조정한다는 의미입니다. 이 정규화 방식은 많이 사용되며 표준화(standardization) 또는 z-점수(z-score) 정규화라고도 부릅니다. 표준화 알고리즘은 간단합니다. 먼저 각 특성의 평균을 계산하고 원래 값에서 빼서 특성의 평균을 0으로 만듭니다. 그다음에는 특성의 표준 편차를 계산하여 나눕니다. 의사 코드로 쓰면 다음과 같습니다.

normalizedFeature = (feature - mean(feature)) / std(feature)

예를 들어 특성 값이 [10, 20, 30, 40]일 때 정규화된 값은 대략 [-1.3, -0.4, 0.4, 1.3]이 됩니다. 확실히 평균은 0이고 대략 보아도 표준 편차는 1입니다. 보스턴 주택 문제에서 정규화 코드는 normalization.js 파일에 들어 있습니다. 이 파일의 내용은 코드 2-9와 같습니다. 여기에는 두 개의 함수가 있습니다. 하나는 전달된 2D 텐서21에서 평균과 표준 편차를 계산하고, 다른 하나는 계산된 평균과 표준 편차로 텐서를 정규화합니다.

 

코드 2-9 데이터 정규화: 평균 0, 단위 표준 편차

/**
 * 데이터 배열에 있는 각 열의 평균과 표준 편차를 계산합니다
 *
 * @param {Tensor2d} data: 각 열의 평균과 표준 편차를 독립적으로 계산하기 위한 데이터셋
 *
 * @returns {Object} 각 열의 평균과 표준 편차를 1d 텐서로 포함하고 있는 객체
 */
export function determineMeanAndStddev(data) {
  const dataMean = data.mean(0);
  const diffFromMean = data.sub(dataMean);
  const squaredDiffFromMean = diffFromMean.square();
  const variance = squaredDiffFromMean.mean(0);
  const dataStd = variance.sqrt();
  return {dataMean, dataStd};
}

/**
 * 평균과 표준 편차가 주어지면 평균을 빼고 표준 편차로 나누어 정규화합니다
 *
 * @param {Tensor2d} data: 정규화할 데이터. 크기: [batch, numFeatures].
 * @param {Tensor1d} dataMean: 데이터의 평균. 크기 [numFeatures].
 * @param {Tensor1d} dataStd: 데이터의 표준 편차. 크기 [numFeatures]
 *
 * @returns {Tensor2d}: data와 동일한 크기의 텐서이지만,
 * 각 열은 평균이 0이고 단위 표준 편차를 가지도록 정규화되어 있습니다
 */
export function normalizeTensor(data, dataMean, dataStd) {
  return data.sub(dataMean).div(dataStd);
}

이 함수를 조금 더 자세히 살펴보겠습니다. determineMeanAndStddev 함수는 입력으로 2D 텐서인 data를 받습니다. 관례상 첫 번째 차원은 샘플 차원입니다. 각 인덱스는 독립적인 고유한 샘플에 대응됩니다. 두 번째 차원은 특성 차원입니다. 두 번째 차원의 12개 원소는 12개 입력 특성(CRIM, ZN, INDUS 등)에 해당합니다. 다음과 같이 독립적으로 각 특성의 평균을 계산할 수 있습니다.

const dataMean = data.mean(0);

이 함수 호출에서 0은 0번째(첫 번째) 차원에 대해 평균을 계산한다는 의미입니다. data가 2D 텐서이므로 두 개의 차원(축)을 가집니다. 첫 번째 축인 배치 축은 샘플 차원입니다. 이 축을 따라 첫 번째, 두 번째, 세 번째 원소로 이동하면서 다른 샘플 또는 다른 부동산 데이터를 참조합니다. 두 번째 차원은 특성 차원입니다. 이 차원의 첫 번째에서 두 번째 원소로 이동하면서 표 2-1에 있는 CRIM, ZN, INDUS 같은 다른 특성을 참조합니다. 축 0을 따라 평균을 구할 때 샘플 방향에 대해 평균을 얻습니다. 계산 결과는 특성 축만 남기 때문에 1D 텐서가 됩니다. 각 특성의 평균을 얻게 됩니다. 만약 축 1에 대해 평균을 계산하면 여전히 1D 텐서를 얻지만 남은 축은 샘플 차원이 됩니다. 이 값은 각 부동산 레코드에 대한 평균에 해당하며 이 예제에서는 의미가 없습니다. 축을 따라 계산할 때 자주 발생하는 에러이므로 올바른 방향으로 계산을 수행하는지 주의하세요.

여기에서 브레이크포인트(breakpoint)22를 설정하면 자바스크립트 콘솔을 사용해 계산된 평균값을 살펴볼 수 있고 전체 데이터셋에 대해 계산한 평균과 매우 가깝다는 것을 알 수 있습니다. 이는 훈련 샘플이 대표성을 가진다는 것을 의미합니다.

> dataMean.shape
[12]
> dataMean.print();
     [3.3603415, 10.6891899, 11.2934837, 0.0600601, 0.5571442, 6.2656188, 68.2264328, 3.7099338, 9.6336336, 409.2792969, 18.4480476, 12.5154343]

다음 코드에서는 (tf.sub()을 사용해) 데이터에서 평균을 빼어 원점에 중앙이 맞춰진 데이터를 얻습니다.

const diffFromMean = data.sub(dataMean);

주의를 100% 기울이지 않는다면 이 코드에 감춰진 마술을 놓칠 수 있습니다. data[333, 12] 크기의 2D 텐서입니다. 반면 dataMean[12] 크기의 1D 텐서입니다. 일반적으로 크기가 다른 두 텐서를 뺄 수 없습니다. 하지만 여기에서는 텐서플로가 브로드캐스팅(broadcasting)을 사용해 두 번째 텐서의 차원을 확장시켜 실제적으로 333번 반복하기 때문에 자세한 지침이 없어도 사용자가 의도한 대로 정확히 수행됩니다. 이런 유용성은 편리하지만, 이따금 브로드캐스팅에 호환되는 크기 규칙이 조금 혼동될 수 있습니다. 브로드캐스팅에 대해 자세히 알고 싶다면 INFO BOX 2.4를 참고하세요.

determineMeanAndStddev 함수에 있는 다른 코드는 특별한 것은 아닙니다. tf.square()는 각 원소의 제곱을 계산하고 tf.sqrt()는 원소의 제곱근을 계산합니다. 각 메서드의 자세한 정의는 TensorFlow.js API 문서(https://js.tensorflow.org/api/latest/)를 참고하세요. 이 문서는 실시간으로 수정할 수 있는 위젯을 포함하고 있어서 그림 2-12와 같이 함수 동작 방식을 직접 테스트할 수 있습니다.

▲ 그림 2-12 TensorFlow.js API 문서(https://js.tensorflow.org/api/latest/) 안에서 TensorFlow.js API를 직접 테스트해 볼 수 있다. 이를 통해 함수 사용법과 특이한 사례를 빠르고 쉽게 이해할 수 있다.

이 예에서 명확한 설명을 우선시하여 코드를 작성했지만 훨씬 더 간결하게 determineMeanAnd Stddev 함수를 작성할 수 있습니다.

const std = data.sub(data.mean(0)).square().mean().sqrt();

텐서플로를 사용하면 불필요한 코드 중복 없이 많은 수치 계산을 표현할 수 있습니다.

INFO BOX 2.4 브로드캐스팅

C = tf.someOperation(A, B)와 같은 텐서 연산을 생각해 보죠. 여기서 AB는 텐서입니다. 가능하고 또 모호하지 않으면 작은 텐서가 큰 텐서의 크기에 맞도록 브로드캐스팅됩니다. 브로드캐스팅은 두 단계로 구성됩니다.

1. 큰 텐서의 랭크에 맞춰 작은 텐서에 (브로드캐스팅되는) 축이 추가됩니다.

2. 큰 텐서의 전체 크기에 맞도록 작은 텐서가 새로운 축을 따라 반복됩니다.

구현 입장에서 보면 매우 비효율적이기 때문에 실제로 새로운 텐서가 만들어지진 않습니다. 반복 연산은 완전히 가상입니다. 메모리 수준이 아니라 알고리즘 수준에서 일어납니다. 하지만 새로운 축을 따라 작은 텐서가 반복된다고 생각하는 것이 이해하기 쉽습니다.

브로드캐스팅은 일반적으로 한 텐서의 크기가 (a, b, ..., n, n + 1, ... m)이고 다른 텐서의 크기가 (n, n + 1, ... , m)인 두 개 텐서의 원소별 연산에 적용할 수 있습니다. 그다음에는 브로드캐스팅이 자동으로 a에서 n - 1 축에 적용됩니다. 예를 들어, 다음 예제 코드는 원소별 maximum 연산을 브로드캐스팅을 사용하여 크기가 다른 두 개의 랜덤한 텐서에 적용합니다.

x = tf.randomUniform([64, 3, 11, 9]); ------ x는 크기가 [64, 3, 11, 9]인 랜덤한 텐서입니다.
y = tf.randomUniform([11, 9]); ------ y는 크기가 [11, 9]인 랜덤한 텐서입니다.
z = tf.maximum(x, y); ------ 출력 z의 크기는 x와 같은 [64, 3, 11, 9]입니다.
신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.