class SimpleANN(nn.Module):
def __init__(self):
super(SimpleANN, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128) # 입력층에서 은닉층으로
self.fc2 = nn.Linear(128, 64) # 은닉층에서 은닉층으로
self.fc3 = nn.Linear(64, 10) # 은닉층에서 출력층으로
def forward(self, x):
x = x.view(-1, 28 * 28) # 입력 이미지를 1차원 벡터로 변환
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
class SimpleANN(nn.Module): torch.nn.Module을 상속하여 모델을 정의합니다. PyTorch에서는 모든 모델이 nn.Module을 기반으로 작성됩니다.
__init__(self): 초기화 메서드입니다. 이 메서드 안에서 모델에 필요한 계층(layer)을 정의합니다.
super(SimpleANN, self).__init__(): 상위 클래스인 nn.Module의 초기화 메서드를 호출합니다. 이를 통해 nn.Module의 기능을 상속받아 사용할 수 있게 됩니다.
nn.Linear는 완전 연결 계층(fully connected layer) 또는 선형 계층이라고 불립니다. 입력 데이터를 선형 변환하여 출력으로 전달하는 역할을 합니다.
nn.Linear(input_dim, output_dim)에서 input_dim은 입력의 차원 수, output_dim은 출력의 차원 수를 의미합니다.
이 계층은 입력 데이터 x에 가중치 W와 편향 b를 곱해 선형 변환을 수행합니다.
데이터 패턴 학습: 신경망은 가중치(W)와 편향(b)을 학습하여 데이터에서 패턴을 발견하고 분류나 예측에 적합한 형태로 변환합니다.
차원 축소 및 확대: 데이터의 차원을 변경하면서 필요한 정보만 남기거나, 더 복잡한 특징을 추출할 수 있게 만듭니다.
다양한 특징 학습: 선형 계층이 여러 개 쌓이면 데이터의 다양한 패턴과 특징을 학습할 수 있습니다. 각 계층은 이전 계층의 출력을 입력받아 점진적으로 더 높은 수준의 정보를 추출합니다.
첫 번째 nn.Linear(784, 128) 계층은 28x28 이미지 픽셀 데이터를 128개의 중요한 특징으로 변환합니다.
다음 nn.Linear(128, 64) 계층은 이 128개의 특징을 더 추상화된 64개의 특징으로 변환합니다.
마지막 nn.Linear(64, 10) 계층은 64개의 특징을 최종 예측값인 10개의 클래스(숫자 0~9)에 해당하는 출력으로 변환합니다.
한번더 설명을 하자면,
self.fc1 = nn.Linear(28 * 28, 128):
첫 번째 완전 연결 계층입니다.
입력 차원이 28 x 28 (784)로, MNIST 이미지의 28x28 픽셀을 1차원으로 펼친 것입니다.
출력 차원은 128로 설정하여, 이 계층을 지나면서 784개의 입력이 128개로 변환됩니다.
self.fc2 = nn.Linear(128, 64):
두 번째 완전 연결 계층입니다.
입력 차원은 128로, 첫 번째 계층의 출력 차원과 일치합니다.
출력 차원은 64로 설정되어, 128개의 입력이 64개로 줄어듭니다.
self.fc3 = nn.Linear(64, 10):
마지막 완전 연결 계층입니다.
입력 차원은 64로 두 번째 계층의 출력과 맞추고, 출력 차원은 10입니다.
여기서 출력 10은 MNIST 데이터셋의 숫자(0~9)를 분류하기 위한 클래스 수를 나타냅니다.
그런데 여기서에서
def forward(self, x): forward 메서드는 신경망의 순전파를 정의합니다. 이 메서드를 통해 입력이 모델의 각 계층을 거쳐 최종 출력이 생성됩니다.
x = x.view(-1, 28 * 28):
MNIST 이미지 데이터를 1차원 벡터로 변환합니다.
.view(-1, 28 * 28)는 이미지 텐서를 (배치 크기, 784)로 형태를 바꾸어줍니다.
-1은 배치 크기(입력된 데이터 샘플 수)에 따라 자동으로 맞추라는 의미입니다.
x = torch.relu(self.fc1(x)):
입력 데이터 x가 첫 번째 완전 연결 계층(fc1)을 통과하여 128차원의 출력을 생성합니다.
torch.relu를 사용하여 ReLU 활성화 함수를 적용합니다. ReLU는 양수 입력을 그대로 출력하고 음수를 0으로 만들어줍니다.
x = torch.relu(self.fc2(x)):
첫 번째 계층의 출력을 두 번째 완전 연결 계층(fc2)에 전달하여 64차원의 출력을 생성합니다.
역시 torch.relu 활성화 함수를 사용하여 출력의 음수를 0으로 변환합니다.
x = self.fc3(x):
두 번째 계층의 출력을 마지막 완전 연결 계층(fc3)에 전달하여 10차원의 출력을 생성합니다.
마지막 계층에서는 활성화 함수를 사용하지 않고 그대로 출력을 반환합니다. 이 출력값은 로짓(logit)이라고 부르며, 소프트맥스나 크로스엔트로피 손실 함수에서 클래스 확률로 변환됩니다.
자고로, 여기서 fc는 "fully connected"의 약자이며, fc1, fc2, fc3 등에서 뒤에 붙는 숫자는 각 계층의 순서를 나타냅니다. 이 신경망에서는 세 개의 fc 계층이 있으므로, fc1, fc2, fc3로 이름을 붙여 순서를 구분하고 있습니다. 각 fc 계층은 앞의 계층에서 나온 특징을 변환하여 다음 계층으로 전달하는 역할을 합니다.
fc1 (첫 번째 계층):
입력으로 28x28 픽셀의 이미지 데이터를 받습니다. 이를 784개의 입력 노드로 간주해, 이를 128개의 뉴런으로 변환합니다.
역할: 이미지의 기본적인 특징을 추출하여 다음 계층에 전달하는 역할을 합니다.
fc2 (두 번째 계층):
fc1에서 나온 128개의 출력을 입력으로 받아 64개의 뉴런으로 변환합니다.
역할: 첫 번째 계층에서 얻은 특징을 더 추상화하여 표현합니다. 데이터가 점점 압축되며, 중요한 정보가 남는 과정입니다.
fc3 (세 번째 계층):
fc2의 64개 출력을 입력으로 받아 10개의 출력으로 변환합니다.
역할: 최종적으로 10개의 클래스로 데이터를 분류하기 위해 각 출력 노드가 하나의 클래스(숫자 0~9)를 나타내게 합니다.
그리고 저기서 x는 입력 데이터를 나타냅니다. 코드에서 x는 주로 MNIST 이미지 데이터가 됩니다. 이 데이터는 이미지를 표현한 숫자들의 모음, 즉 픽셀 값 배열이 됩니다. 신경망은 이미지를 직접 이해하는 대신, 픽셀 값을 수치 데이터로 변환해 학습을 진행합니다.
제일 중요한 점은, self.fc1은 단순한 변수가 아니라, PyTorch의 nn.Linear 객체로 생성된 레이어(layer) 인스턴스입니다. 이 레이어는 마치 함수처럼 입력 데이터를 받을 수 있게 만들어졌습니다.
nn.Linear 객체는 Python에서 함수 호출 연산자 ()를 오버로드하고 있어, 인자를 넣으면 레이어의 연산이 자동으로 수행되도록 설계되어 있습니다. 즉, self.fc1(x)라고 쓰면 내부적으로 레이어의 파라미터(가중치와 편향)가 x에 적용되어 결과값을 반환합니다.
객체 생성: self.fc1 = nn.Linear(28 * 28, 128)는 입력 크기 28*28과 출력 크기 128을 가지는 선형 레이어(nn.Linear)를 생성합니다.
이 과정에서 가중치 행렬과 편향이 무작위로 초기화됩니다.
입력 받기: self.fc1(x)처럼 x를 괄호 안에 넣고 호출하면, nn.Linear 레이어가 x에 대해 행렬 곱을 수행하고, 편향을 더한 결과를 반환합니다.
여기서 x를 계속 쓰는 이유는,
입력 데이터 변형:
x = x.view(-1, 28 * 28)는 입력 이미지 데이터를 1차원 벡터로 변형합니다. 이렇게 하면 각 이미지의 784개의 픽셀 값이 한 줄로 나열되어 첫 번째 레이어(fc1)에 전달됩니다.
첫 번째 완전 연결층 (fc1):
self.fc1(x)는 784개의 입력 노드를 가지고 128개의 출력을 생성합니다.
이 출력은 다음 레이어에 입력으로 사용되며, 여기서 x는 128개의 값으로 변환됩니다.
은닉층에서의 x 사용:
이제 x는 첫 번째 완전 연결층의 출력을 나타냅니다. 즉, x는 이제 128개의 값으로 구성된 텐서입니다. 이 값은 다음 레이어(fc2)에 전달됩니다.
x = torch.relu(self.fc1(x)) 후에 x는 (배치 크기, 128) 형태가 됩니다.
두 번째 완전 연결층 (fc2):
self.fc2(x)는 128개의 입력 노드를 가지고 64개의 출력을 생성합니다.
이 출력은 x에 저장되어, 이제 x는 64개의 값으로 변환됩니다.
세 번째 완전 연결층 (fc3):
self.fc3(x)는 64개의 입력 노드를 가지고 10개의 출력을 생성합니다.
이 출력은 신경망의 최종 결과로, x는 이제 10개의 클래스(숫자 0~9)에 대한 예측값을 가집니다.
도전과제
#데이터 전처리
def preprocess_text(content):
if isinstance(content, float):
return ''
content = content.lower()
content = re.sub(r'[^/w/s]', '', content)
content = re.sub(r'\d+', '', content)
content = content.strip()
return content
data[['userName', 'content']] = data[['userName', 'content']].apply(lambda col: col.apply(preprocess_text))
def preprocess_score(score):
return int(score)
class ReviewDataset(Dataset):
def __init__(self, content, score, preprocess_text, preprocess_score):
self.content = content
self.score = score
self.preproces_text = preprocess_text
self. preprocess_score = score
def __len__(self):
return len(self.content)
def __getitem__(self, idx):
content = self.preprocess_text(self.content[idx])
score = self.preprocess_score(self.score[idx])
return torch.tensor(content), torch.tensor(score
train_content, test_content, train_score, test_score = train_test_split(
data['content'], data['score'], test_size=0.2, random_state=42)
batch_size = 64
train_dataset = ReviewDataset(train_content, train_score, preprocess_text, preprocess_score)
train_dataload = DataLoader(train_dataset, batch_size = batch_size, shuffle = True)
test_dataset = ReviewDataset(test_content, test_score, preprocess_text, preprocess_score)
test_dataload = DataLoader(test_dataset, batch_size = batch_size, shuffle = True)
embedding_layer = nn.EmbeddingBag(vocab_size, embed_dim, mode='mean')
vocab_size: 전체 단어 집합의 크기입니다. 각 단어에 고유한 임베딩 벡터가 할당됩니다.
embed_dim: 각 단어를 임베딩할 벡터의 차원입니다.
mode: 단어 임베딩을 결합하는 방법을 지정합니다. 주로 'mean', 'sum', 'max' 등이 사용됩니다.
'mean': 임베딩 벡터의 평균을 반환합니다.
'sum': 임베딩 벡터의 합을 반환합니다.
'max': 각 차원에서 가장 큰 값을 반환합니다.
임베딩(Embedding)은 텍스트와 같은 비정형 데이터(예: 단어, 문장, 문서)를 벡터 형태의 숫자로 변환하여 컴퓨터가 처리할 수 있도록 하는 기술입니다. 머신러닝이나 딥러닝 모델이 텍스트를 학습하고 이해하려면, 텍스트를 수치적으로 표현하는 방법이 필요합니다. 이때, 임베딩을 통해 단어나 문장을 고정된 크기의 벡터로 변환합니다.
임베딩이 필요한 이유
컴퓨터는 텍스트를 그대로 이해할 수 없습니다. 단어를 숫자로 표현하려면:
단어마다 고유한 인덱스를 부여할 수도 있지만, 이는 단어 간의 관계를 반영하지 못합니다. (예: "cat"과 "dog"은 다른 인덱스를 가지지만, 유사한 의미를 지니는 단어)
임베딩을 사용하면 비슷한 의미를 가진 단어들이 가까운 벡터 공간에 위치하게 되어, 모델이 더 잘 학습할 수 있습니다.
nn.Embedding이나 nn.EmbeddingBag에서 sparse=True는 희소 행렬(sparse matrix) 방식을 사용할지 여부를 설정하는 옵션입니다. 이 옵션을 활성화하면, 희소 행렬 기법을 통해 메모리 사용량과 연산 효율을 개선할 수 있습니다.
희소 행렬(sparse matrix)란?
희소 행렬은 대부분의 요소가 0인 행렬로, 대형 데이터에서 메모리 효율을 높이기 위해 널리 사용됩니다. 예를 들어, 단어 수가 아주 많은 데이터에서 특정 몇몇 단어만 활성화된 벡터가 생성되면, 나머지 대부분의 값은 0이 됩니다. 희소 행렬은 이런 0 값을 저장하지 않고, 0이 아닌 값의 위치와 그 값만을 저장하여 메모리를 절약합니다
nn.LSTM의 주요 매개변수
nn.LSTM에서 자주 사용하는 매개변수와 이들이 의미하는 바는 다음과 같습니다.
input_size: LSTM으로 입력되는 각 시점의 데이터 차원 (입력 벡터의 크기).
hidden_size: LSTM의 은닉 상태 벡터 크기, 즉 메모리 셀의 차원. 이 크기가 클수록 LSTM이 더 많은 정보를 기억할 수 있지만 계산 비용도 증가합니다.
num_layers: LSTM 레이어의 수. 1로 설정하면 단층 LSTM이 되고, 2 이상으로 설정하면 여러 LSTM 층을 쌓아서 더 깊은 네트워크를 만들 수 있습니다.
batch_first: True로 설정하면 입력의 첫 번째 차원이 배치 크기로 설정됩니다 (batch_size, seq_len, input_size 형식). 이 설정은 데이터가 배치 크기 단위로 들어올 때 편리합니다.
output, (hidden, cell) = self.lstm(embedded.unsqueeze(0))
nn.LSTM은 LSTM의 특성상 무조건 (hidden, cell) 두 가지 상태를 반환합니다. 이 두 상태는 LSTM의 구조상 필수적인 정보로, 다음과 같은 이유로 항상 반환됩니다:
LSTM 구조: LSTM은 순환 신경망(RNN)의 한 종류로, 장기적인 종속성을 잘 학습하기 위해 은닉 상태와 셀 상태라는 두 가지 상태를 유지합니다. 은닉 상태는 단기 정보를, 셀 상태는 장기 정보를 보존하기 때문에, 두 상태를 모두 반환해야 LSTM의 기능을 제대로 사용할 수 있습니다.
상태 전달: 모델을 통해 입력 시퀀스의 각 타임스텝을 거치면서 이전의 hidden과 cell 상태가 다음 타임스텝으로 전달됩니다. 이로 인해 모델의 기억 기능이 가능해집니다.
순환 구조의 일관성: PyTorch의 LSTM 레이어는 항상 두 상태를 반환하는 것으로 표준화되어 있습니다. 그래서 모델이 학습 시에 상태 정보를 관리하고, 다른 시퀀스 또는 배치로 넘어갈 때 효율적으로 사용할 수 있습니다.
unsqueeze(0)은 PyTorch에서 텐서의 차원을 추가하는 연산으로, 기존 텐서의 형태에 새로운 차원을 넣어줍니다. 여기서 0은 첫 번째 차원을 의미하며, 이를 통해 텐서가 원하는 모양을 가지도록 변형할 수 있습니다.
예를 들어, embedded.unsqueeze(0)는 embedded 텐서의 맨 앞에 새로운 차원을 추가하는 것입니다.
embedded = torch.tensor([1, 2, 3, 4]) # shape: [4] embedded = embedded.unsqueeze(0) # shape: [1, 4]
unsqueeze(0)에서의 숫자 0은 차원을 추가할 위치를 의미합니다.
차원에 대한 설명
차원이란?: 차원은 데이터의 구조를 나타내는 개념입니다. 예를 들어:
0차원: 단일 값, 즉 스칼라. (예: 5)
1차원: 배열 또는 리스트. (예: [1, 2, 3])
2차원: 행렬. (예: [[1, 2, 3], [4, 5, 6]])
unsqueeze(0)의 의미:
unsqueeze(0)은 0번째 차원에 빈 차원을 추가하는 것입니다.
이렇게 하면 기존의 데이터를 감싸는 형태로 바뀌게 됩니다.
hidden:
LSTM은 입력 시퀀스를 처리하고, 각 시점에서 **숨겨진 상태(hidden state)**를 반환해요.
hidden은 LSTM의 마지막 시점에서의 숨겨진 상태를 포함하는 텐서입니다.
hidden[-1]은 마지막 시점의 숨겨진 상태를 의미해요. LSTM은 여러 시점을 처리하기 때문에, hidden은 보통 여러 단계의 숨겨진 상태를 가지고 있어요. -1을 사용하면 마지막 단계의 값을 선택합니다.
self.fc:
self.fc는 fully connected layer (완전 연결층)입니다. 이 레이어는 입력받은 값을 다른 차원의 출력으로 변환해줘요.
일반적으로 신경망의 마지막에 위치하며, 예측을 하기 위해 사용됩니다.
return self.fc(hidden[-1]):
hidden[-1]은 LSTM의 마지막 숨겨진 상태를 입력으로 사용해, self.fc 레이어를 통해 예측값을 생성합니다.
결과적으로, 이 코드는 LSTM이 처리한 입력의 결과로서 예측값을 반환합니다.
여기까지