1. OOP Basics
Class
- 제품의 설계도
Object
- 설계도로 만든 제품
- 하나의 Class로부터 여러 개의 객체를 생성할 수 있음
OOP Terms
- Class: 제품의 설계도
- Object: 설계도로 만든 제품
- Attribute(member, 속성): Class 안의 변수
- Method(메소드): Class 안의 함수
- Constructor(생성자, __init__): Object 생성시 실행되는 함수
- Instance(인스턴스): 메모리 내에 살아있는 객체
Class 만들기
- Constructor & Method
class Pokemon:
def __init__(self, name, types): # Constructor
self.name = name
self.types = types
def say(self): # method
print(f"{self.types} type pokemon {self.name}")
charmander = Pokemon("charmander", "fire")
charmander.say()
fire type pokemon charmander
Getter & Setter Method
- Getters: Methods to acces data members (접근)
- 특정한 속성의 값(Attribute)에 접근할 때 사용되는 Method
- Setters: Methods to modify data members (수정)
- 특정한 속성의 값(Attribute)을 변경할 때 사용되는 Method
class Pokemon:
def __init__(self, name, types): # Constructor
self.name = name
self.types = types
def say(self): # method
print(f"{self.types}-type pokemon {self.name}")
# Setter Method
def set_name(self, name, types):
self.name = name
self.types = types
charmander = Pokemon("charmander", "fire")
charmander.set_name("Grovyle", "Grass")
charmander.say()
Grass-type pokemon Grovyle
Destructor
- Instance가 소멸되었을 때 처리해주는 함수
- Magic Method: __del__(self) 으로 처리
- del Object_Name 으로 메모리 공간의 할당을 해제
class Pokemon:
def __init__(self, name, types): # Constructor
self.name = name
self.types = types
def say(self): # method
print(f"{self.types}-type pokemon {self.name}")
# Setter Method
def set_name(self, name, types):
self.name = name
self.types = types
def __del__(self):
print("Deleting the instance")
charmander = Pokemon("charmander", "fire")
charmander.set_name("Grovyle", "Grass")
charmander.say()
del charmander
charmander.say()
Grass-type pokemon Grovyle
Deleting the instance
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[1], line 22
20 charmander.say()
21 del charmander
---> 22 charmander.say()
NameError: name 'charmander' is not defined
Class Inheritance
- 다른 Class의 Attribute나 Method를 물려받아 사용하는 기법
- Parent & Child Relationship이 존재
- Child Class: Parent Class를 상속받은 Class
- super(child_class_name, self).__init__(parent_class_attributes): 부모 Class의 Constructor 호출
- Child Class의 생성자 (__init__) 안에서 호출
- super().parent_class_method: 부모 Class의 Method 사용
class Pokemon:
def __init__(self, name, tech): # Constructor
self.name = name
self.tech = tech
def attack(self):
print(f"{self.name} 이(가) {self.tech}를 사용했습니다!")
class Monster(Pokemon):
def __init__(self, name, tech, level):
super(Monster, self).__init__(name, tech)
self.level = level
def show_info(self):
print(f"이름: {self.name}, 레벨: {self.level}")
pikachu = Monster("피카츄", "전광석화", 10)
pikachu.show_info()
pikachu.attack()
이름: 피카츄, 레벨: 10
피카츄 이(가) 전광석화를 사용했습니다!
2. OOP Advanced
oop란?
- 객체지향 프로그래밍
- 기본의 만들었던 내용을 재사용할 수 있다는 장점이 있음
- 프로그램을 독립된 단위인 객체들의 모임으로 보고 각각 객체는 메시지를 주고 받고 데이터를 처리함
'''
1. 추상화(Abstraction) : 핵심적인 코드만 보여주기
< 의존 대상을 추상 타입으로 간접 참조하고 사용하고 있는 의존 대상의 변경이 사용하는 입장에는 영향을 주지 않는다 >
- 불필요한 부분을 숨긴다.
- 인터페이스와 구현을 분리한다.
• 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정을 추상화라 한다.
• 구현 세부사항 대신 기능 측면에서 클래스를 개발하는 것이다.
• 추상화의 목적은 클래스 구현 세부 사항과 동작을 분리하는 것을 목표로 한다.
• 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
• 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.
• 추상화를 통해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미.
- 타입 추상화
•여러 구현 클래스를 대표하는 상위 타입 도출 ex) Notifier
•흔히 인터페이스 타입으로 추상화
•추상화 타입과 구현은 타입 상속으로 연결 (implements, extends)
•사용할 대상을 추상화를 하면 사용 대상이 변경되어도 사용하는 코드에서는 변화하지 않아도 된다.
- 추상화는 의존 대상이 변경되는 시점에 한다
• 무분별한 추상화는 전체 추상 타입 증가와 이로 인한 코드 복잡도 증가를 불러일으킨다.
• 아직 존재하지 않는 기능에 대한 이른 추상화는 주의해야 한다.
• 실제 변경, 확장이 발생할 때 추상화를 시도하는 것이 좋다.
2. 캡슐화(encapsulation) : 데이터 보호
< 기능 구현을 외부로부터 감추고, 내부의 구현 변경이 외부로 전파되는 것을 막아준다 >
- 데이터 캡슐화 : 필드와 메서드를 하나로 묶는 것
- 은닉화 : 객체의 세부 내용이 외부에 드러나지 않아 외부에서 데이터를 직접 접근하는 것을 방지한다.
• 객체가 내부적으로 기능을 어떻게 구현했는지 감추는 것
• 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제한다.
• 변경될 가능성이 높은 부분을 '구현'이라고 하고 상대적으로 안정적인 부분을 '인터페이스'라고 한다.
• 외부에 영향을 주지 않고 객체 내부의 '구현'을 변경할 수 있게 함
3. 상속(inherutance) : 코드 재사용
- 자식클래스가 부모클래스의 특징과 기능을 물려받는 것
- 클래스를 상속 받아 수정하여 사용하기 때문에 중복 코드를 줄일 수 있다.
- 부모클래스의 수정으로 모든 자식클래스들도 수정되는 효과
- 클래스에 메소드 추가가 어려운 경우 사용
- 자신이 아닌 남이 만든 클래스를 가져오는 경우 (수정 불가)
- 클래스가 다양한 곳에서 상속 받아 쓰이는 경우 (메서드를 추가하면 다른 곳에서 불필요한 기능이 포함될 수 있음)
4. 다형성(polymorphism) : 객체 변경 용이
- 어떤 변수, 메소드가 상황에 따라 다른 결과를 내는 것
<다형성을 가능하게 하는 것들>
- 오버라이딩(overriding) : 부모클래스 메서드를 자식클래스에서 재정의하는 것
- 오버로딩(overloading) : 한 클래스에서 메소드 이름은 같지만 파라미터 개수나 자료형을 다르게 하여 서로 다르게 동작하게 하는 것
# 클래스 : 반복되는 변수 & 메서드(함수)를 미리 정해놓은 틀
# 대문자로 시작 : class
# 소문자로 시작 : ()있으면 함수, ()없으면 변수
# class 쓰는 방법
# class 입력 -> 대문자로 시작하는 클래스의 이름을 작성 -> 안에 들어갈 함수와 변수 설정
# method = class 안에 있는 함수
# self = class 로 찍어낸 객체('인스턴스화') / 함수의 ()안에 self가 들어감
# OOP(객체 지향 프로그래밍), Self, 인스턴스 메소드, 인스턴스 변수
# 클래스 and 인스턴스 차이 이해
# 네임스페이스 : 객체를 인스턴스화 할 때 저장된 공간
# 클래스 변수 (class 안에 독립적으로 존재하는 변수) : 직접 접근 가능, 공유
# 인스턴스 변수 (class안의 함수의 괄호 안 변수들 ) : 객체마다 별도 존재
'''
# < Object - Oriented Programming Characteristics >
# - Inheritance(상속), Polymorphism, Encapsulation(캡슐화), Abstraction(추상화)
# 1. < Inheritance >
# From the Programming aspect, inheritance generally means “inheriting or transfer of characteristics from parent to child class without any modification”
# Ever heard of this dialogue from relatives “you look exactly like your father/mother” the reason behind this is called ‘Inheritance.‘
# 2. < Polymorphism >
# Polymorphism defines the ability to take different forms.
# Polymorphism in Python allows us to define methods in the child class with the same name as defined in their parent class.
# 3. < Encapsulation >
# Encapsulation describes the idea of wrapping data and the methods that work on data within one unit.
# Encapsulation puts restrictions on accessing variables and methods directly and can prevent the accidental modification of data.
# Encapsulation is highly relevant to information hiding.
# 4. < Abstraction >
# Abstraction in python is defined as a process of handling complexity by hiding unnecessary information from the user.
# Abstraction enables the user to implement even more complex logic on top of the provided abstraction without understanding or even thinking about all the hidden background/back-end complexity.
3. Class Inheritance
1) Inheritance
- class Child Class(Parent Class):
- 계승방향은 무조건 <- 방향!!
'''
- Create a class named Person
- Attributes : firstname, lastname properties
- printname method
'''
class Person :
def __init__(self,firstname,lastname) :
self.firstname = firstname
self.lastname = lastname
def printname(self) :
print(f"The person's name is {self.firstname} {self.lastname}")
p = Person("Kim", "Jaewon")
p.printname()
class Student(Person) :
pass
p_new = Student("Kim","Jinhyeong")
p_new.printname()
2) __init__():
- single inheritance 시 자식 클래스에서 __init__(Constructor)을 정의하면 다시 상속해줘야 함
- 자식 클래스에서 __init__을 정의할 때 부모 클래스에서 __init__을 호출
- def __init__(self, Parent Attributes):
- ParentClass.__init__(self, Parent Attributes)
- super( ).__init__(Parent Attributes)
- super(Child_class, self).__init__(Parent Attributes)
- 부모 Class로부터 상속받을 때 __init__()의 괄호 안에 부모 Class에 있던 변수가 전부 들어가야 함
# def __init__(self, .. , ...) :
# 부모 클래스.__init__(self, ... , ...)
# Whenever a class is created (called for the first time to create a new object),
# __init__() function is called automatically.
# When you add the __init__() function,
# the child class will no longer inherit the parent's __init__() function.
# To keep the inheritance of the parent's __init__() function, add a call to the parent's __init__() function:
class Student(Person) :
def __init__(self,firstname,lastname) :
Person.__init__(self,firstname,lastname)
3) super() Function
- 부모 클래스에 있는 모든 method와 attribute를 모두 가져감
- def __init__(self, Parent Attributes):
- ParentClass.__init__(self, Parent Attributes)
- super( ).__init__(Parent Attributes)
- super(Child_class, self).__init__(Parent Attributes)
- 부모 Class로부터 상속받을 때 __init__()의 괄호 안에 부모 Class에 있던 변수가 전부 들어가야 함
- _init__() 안에 self가 안 들어감 -> 굳이 부모 클래스를 명시해줄 필요가 없음
# super() function makes the child class inherit all the methods and properties from its parent.
# By using the super() function, you do not have to use the name of the parent element,
# it will automatically inherit the methods and properties from its parent.
class Student(Person) :
def __init__(self, firstname, lastname, year) :
super().__init__(firstname, lastname)
self.graduation = year
def welcome(self) :
print(f"Welcome {self.firstname} {self.lastname} to the class of {self.graduation}")
new_std = Student("Kim","Jaewon",2022)
new_std.welcome()
4) Multi- Level Inheritance : 1개 -> 1개 -> 1개
- parent_class.__init__(self, a, b,...) : 부모 클래스의 __init__ method를 가져옴
- super(child_class, self).__init__(a, b,...) : 부모 클래스의 모든 method, attribute를 가져옴
- Parent 클래스로부터 상속할 때 : __init__의 괄호() 안에 부모 클래스에 있던 변수가 다 들어가있어야 함!!
class Parent :
'member of parent class'
'methods of parent class'
pass
class child1(Parent) :
'member of child1 class + parent class'
'methods of chil1 class + parent class'
pass
class child2(child1) :
'member of child2 class + child1 class + parent class'
'methods of child2 class+ chil1 class + parent class'
pass
class Parent :
def __init__(self,name) :
self.name = name
def getName(self) :
return self.name
class Child(Parent) :
def __init__(self,name,age) :
Parent.__init__(self,name)
self.age = age
def getAge(self) :
return self.age
class Grandchild(Child) :
def __init__(self,name,age,location) :
Child.__init__(self,name,age)
self.location = location
def getLocation(self) :
return self.location
me = Parent("Jaewon")
other = Child("Jinhyeong",22)
third_party = Grandchild("HIHI",24,"Pangyo")
print(me.getName())
print(other.getName(),other.getAge())
print(third_party.getName(),third_party.getAge(),third_party.getLocation())
5) Multi- Level Inheritance 2
- super(child_class, self).__init__(Parent_Attributes) : 상속받은 부모 클래스의 __init__ method를 모두 실행
class Xyz :
def __init__(self) :
print(f"Hey, I'm initialized Xyz")
def sub_Xyz(self,b) :
print(f"Printing from class Xyz: {b} ")
class Xyz1(Xyz) :
def __init__(self) :
print(f"Hey, I'm initialized Xyz1")
super().__init__() # self는 안 들어감
def sub_Xyz1(self,b) :
print(f"Printing from class Xyz1: {b} ")
super().__init__()
class Xyz2(Xyz1) :
def __init__(self) :
print(f"Hey, I'm initialized Xyz2")
super().__init__()
def sub_Xyz2(self,b) :
print(f"Printing from class Xyz2: {b} ")
super().__init__()
if __name__ == '__main__' :
ob=Xyz2()
ob.sub_Xyz2(3)
6) Hierarchical Inheritance 1 : 1개 -> 2개
- Parent_class.__init__(self, a, b,...) : 부모 클래스의 __init__ method를 가져옴
- super(Child_class, self).__init__(a, b,...) : 부모 클래스의 모든 method, attribute를 가져옴
- 부모 클래스로부터 상속할 때 : __init__의 괄호() 안에 부모 클래스에 있던 변수가 다 들어가있어야 함!!
# Parent Class
class Pet :
def __init__(self,pet_type,name,bdate) :
self.pet_type = pet_type
self.name = name
self.bdate = bdate
def details(self) :
print("I am pet!")
# Child Class 1
class Cat(Pet) :
def __init__(self,pet_type,name,bdate) :
self.name = "Grey " + name
self.pet_type = pet_type
self.bdate = bdate
def details(self) :
print("I am a cute pet", self.pet_type, 'people call me', self.name)
cat = Cat("cat","catty",'2021-03-02')
cat.details()
print(cat.name)
print(cat.pet_type)
print(cat.bdate) # Attribute Error
# Child Class2
class Dog(Pet) :
def __init__(self,pet_type,name,bdate,breed) :
super().__init__(pet_type,name,bdate)
self.breed = breed
def sounds(self, sound) :
return sound
def details(self) :
print(f"I am {self.name} a {self.breed}")
pet1 = Pet('cat', 'Tiffiny', '2019-07-08')
pet2 = Cat('cat', 'Gatsby', '2018-07-08')
pet3 = Dog('dog', 'Toby', '2018-07-08', 'bull dog')
pet4 = Dog('dog', 'Max', '2018-07-08', 'Tibetan Mastiff')
print(pet1.name)
pet1.details()
print(pet2.name, "is a chubby", pet2.pet_type)
pet2.details()
print(pet3.name, "is a", pet3.breed, "and it always", pet3.sounds("growls"))
pet3.details()
pet4.details()
7) Multiple Inheritance Syntax : n개 -> 1개
class Base1:
'Body of the class'
pass
class Base2:
'Body of the class'
pass
class Derived(Base1,Base2):
'Body of the class'
pass
# 8. Multiple Inheritance Example
class Car() :
def audi(self) :
print("This is an audi car")
class Bike() :
def harleydavidson(self) :
print("This is a Harley Davidson")
class Bus() :
def Benz(self) :
print("This is a Benz")
class Truck() :
def eicher(self) :
print("Eicher Truck here")
class Plane() :
def KoreanAir(self) :
print("Korean air all the way")
# derived classs with multiple base classes - Car, Bike, Bus, Truck, Plane
class Transport(Car, Bike, Bus, Truck, Plane):
def main(self):
print("Main class")
obj = Transport()
obj.audi()
obj.harleydavidson()
obj.Benz()
obj.eicher()
obj.KoreanAir()
obj.main()
# 9. Multiple Inheritance Example 2
class Class1:
def m(self):
print("In Class1")
class Class2(Class1):
def m(self):
print("In Class2")
class Class3(Class1):
def m(self):
print("In Class3")
class Class4(Class2, Class3):
pass
print()
obj = Class4()
obj.m()
10) Method Resolution Order(MRO)
- 다중 상속의 경우, 주어진 속성은 현재 클래스에서 먼저 찾고, 없을 경우에는 부모 클래스에서 찾는다
- 다중 상속시 search의 부모 클래스 우선순위는 왼쪽 -> 오른쪽 순서이다.
- (DFS X, BFS 느낌)
# In Python, every class whether built-in or user-defined is derived from the object class and all the objects are instances of the class object.
# In the case of multiple inheritance, a given attribute is first searched in the current class if it’s not found then it’s searched in the parent classes.
# The parent classes are searched in a left-right fashion and each class is searched once.
# The order that is followed is known as a linearization of the class Derived and this order is found out using a set of rules called
# Method Resolution Order (MRO).
class Class1:
def m(self):
print("In Class1")
class Class2(Class1):
pass
class Class3(Class1):
def m(self):
print("In Class3")
class Class4(Class2, Class3):
pass
print()
obj = Class4()
obj.m()
# 11. Method Resolution Order 2
# To view the MRO of a class,
# Use the mro() method, it returns a list
# ex) Class4.mro()
# Use the _mro_ attribute, it returns a tuple ex) Class4.__mro__
class Class1:
def m(self):
print("In Class1")
class Class2(Class1):
def m(self):
print("In Class2")
class Class3(Class1):
def m(self):
print("In Class3")
class Class4(Class2, Class3):
def m(self):
print("In Class4")
print()
obj = Class4()
obj.m()
Class2.m(obj)
Class3.m(obj)
Class1.m(obj)
# 12. Super function ex
class Class1:
def m(self):
print("In Class1")
class Class2(Class1):
def m(self):
print("In Class2")
super().m()
class Class3(Class1):
def m(self):
print("In Class3")
super().m()
class Class4(Class2, Class3):
def m(self):
print("In Class4")
super().m()
print()
obj = Class4()
obj.m()
# 13. Super function ex 2
class Class1:
def m(self):
print("In Class1")
class Class2(Class1):
def m(self):
print("In Class2")
super().m()
class Class3(Class1):
def m(self):
print("In Class3")
super().m()
class Class4(Class2, Class3):
def m(self):
print("In Class4")
super().m()
print()
print(Class4.mro())
print(Class4.__mro__)
4. PyTorch Basic
PyTorch
- Meta에서 발표한 DL 구현을 위한 Python 기반 OpenSource Library
- Numpy를 대체하면서 GPU를 이용한 연산 가능
- Dynamic Computing Graph (Define by Run) 으로 진행
Tensor
- Tensor는 PyTorch에서 데이터를 표현하기 위해 사용하는 기본 구조
- Numpy의 ndarray와 유사, GPU를 사용하여 연산 가능
import torch
device = torch.device("mps:0" if torch.backends.mps.is_available() else "cpu")
print(torch.tensor([[1, 2], [3, 4]]))
print(torch.tensor([[1, 2], [3, 4]], device=device))
print(torch.tensor([[1, 2], [3, 4]], dtype=torch.float64))
tensor([[1, 2],
[3, 4]])
tensor([[1, 2],
[3, 4]], device='mps:0')
tensor([[1., 2.],
[3., 4.]], dtype=torch.float64)
tensor vs Tensor
- torch.tensor (Python function, in torch.tensor)
- torch.Tensor (Python class, in torch.Tensor)
Type의 차이
- torch.tensor: Input Data에 따라 type이 변함
- torch.Tensor: float로 Data Type 고정
array = np.array([1, 2, 3])
Tensor = torch.Tensor(array)
tensor = torch.tensor(array)
print(Tensor)
print(Tensor.dtype)
print(tensor)
print(tensor.dtype)
tensor([1., 2., 3.])
torch.float32
tensor([1, 2, 3])
torch.int64
Scalar 값이 들어왔을 때의 차이
- torch.tensor: 단순 Scalar 값도 하나의 data로 인식함
- torch.Tensor: List안에 n개의 data가 랜덤으로 들어감
print(torch.Tensor(3), torch.Tensor(3).dtype)
print(torch.tensor(3), torch.tensor(3).dtype)
tensor([0., 0., 0.]) torch.float32 # 매우 작은 값이라 차이 식별 불가
tensor(3) torch.int64
requires_grad (자동미분) 차이: Autograd 할 때
- torch.tensor: requires_grad 가 function의 parameter로 들어감
- torch.Tensor: requires_grad 가 Class의 method로 들어감
print(torch.tensor([2.0, 3.0], requires_grad=True))
print(torch.Tensor([2.0, 3.0]).requires_grad_(True))
tensor([2., 3.], requires_grad=True)
tensor([2., 3.], requires_grad=True)
Data가 없는 경우
- torch.tensor: data가 반드시 존재해야 만들 수 있음
- torch.Tensor: data가 없어도 생성 가능함 (빈 tensor가 생성됨)
torch.Tensor()
tensor([])
torch.tensor()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[65], line 1
----> 1 torch.tensor()
TypeError: tensor() missing 1 required positional arguments: "data"
Indexing & Slicing
- Python list 와 동일
temp = torch.FloatTensor([1, 2, 3, 4, 5, 6, 7])
print(temp[0], temp[1], temp[-1]) # Indexing
print("Enter")
print(temp[2:5], temp[4:-1]) # Slicing
tensor(1.) tensor(2.) tensor(7.)
Enter
tensor([3., 4., 5.]) tensor([5., 6.])
4 Arithmetic Operations
- Element - Wise Operation: 동일한 자리에 위치한 원소끼리 연산 수행
v = torch.tensor([1, 2, 3])
w = torch.tensor([4, 5, 6])
print(torch.add(v, w))
print(torch.sub(v, w))
print(torch.mul(v, w))
print(torch.div(v, w))
tensor([5, 7, 9])
tensor([-3, -3, -3])
tensor([ 4, 10, 18])
tensor([0.2500, 0.4000, 0.5000])
Dimension Handling
- view: tensor의 shape을 reshape
- numpy의 reshape method와 동일
- view(-1): 1D tensor로 flatten 시킨다
- transpose: 두 개의 차원만 맞바꾸어 전치 수행
- permute: 여러 차원(모든 차원)을 재배치
- permute(0, 2, 1): 원래 순서가 0, 1, 2인데 dim=1과 dim=2의 parameter만 switch
- 새롭게 생기는 shape의 dim이 0이 된다 :
- depth(dim=0), x(dim=1), y(dim=2)
- 1차원 tensor는 dim=0(axis=0) 만 존재
- torch의 dim은 ndarray의 axis와 동일한 역할을 수행
temp = torch.tensor([[1, 2], [3, 4]])
print(temp)
print(temp.shape)
print()
print(temp.view(4, 1))
print(temp.view(4, 1).shape)
print()
print(temp.view(-1))
print(temp.view(-1).shape)
print()
print(temp.view(2, -1))
print(temp.view(2, -1).shape)
print()
tensor([[1, 2],
[3, 4]])
torch.Size([2, 2])
tensor([[1],
[2],
[3],
[4]])
torch.Size([4, 1])
tensor([1, 2, 3, 4])
torch.Size([4])
tensor([[1, 2],
[3, 4]])
torch.Size([2, 2])
temp = torch.tensor(range(1, 7)).view(2, 3)
print(temp)
print(temp.transpose(0, 1))
temp2 = torch.tensor(range(1, 7)).view(1, 2, 3)
print(temp2)
print(temp2.permute(0, 2, 1))
tensor([[1, 2, 3],
[4, 5, 6]])
tensor([[1, 4],
[2, 5],
[3, 6]])
tensor([[[1, 2, 3],
[4, 5, 6]]])
tensor([[[1, 4],
[2, 5],
[3, 6]]])
- squeeze: dimension 중 size가 1인 차원을 제거하여 tensor의 차원을 축소
- unsqueeze(dim): 지정한 dim parameter에 size가 1인 dimension을 추가
a = torch.tensor([[1, 2, 3]])
print(a.shape)
print(a.squeeze())
print(a.squeeze().shape)
print(a.unsqueeze(2))
print(a.unsqueeze(2).shape)
torch.Size([1, 3])
tensor([1, 2, 3])
torch.Size([3])
tensor([[[1],
[2],
[3]]])
torch.Size([1, 3, 1])
- torch.cat([a, b], dim): 주어진 tensor를 dim 방향에 따라 concatenate 시킴 (차원 증가 X)
- torch.stack([a,b], dim): 주어진 tensor를 dim 방향에 따라 stack 시킴 (차원 추가)
a = torch.tensor([[1, 2, 3]])
b = torch.tensor([[4, 5, 6]])
print(torch.cat([a, b], dim=1))
print(torch.stack([a, b], dim=0))
tensor([[1, 2, 3, 4, 5, 6]])
tensor([[[1, 2, 3]],
[[4, 5, 6]]])
- torch.tensor.scatter_(dim, index, src)
- dim: scatter 할 기준이 되는 축 (2차원 기준 0이면 아래 방향, 1이면 우측 방향)
- index: scatter할 element들이 배정되는 index
- src: scatter할 source element들
- dim=0 이면 index의 shape이 (1, K) 형태여야 함 (가로로 길게 늘어져 index가 하나씩 아래로 배정)
- dim=1 이면 index의 shape이 (K, 1) 형태여야 함 (세로로 길게 늘어져 Index가 하나씩 우측으로 배정)
src = torch.arange(1, 11).reshape((2, 5))
src
>> tensor([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10]])
index = torch.tensor([[0, 1, 2, 0]])
torch.zeros(3, 5, dtype=src.dtype).scatter_(0, index, src)
>> tensor([[1, 0, 0, 4, 0],
[0, 2, 0, 0, 0],
[0, 0, 3, 0, 0]])
5. PyTorch Model Implementation
1) DataSet 준비
- 단순 FIle 불러오기
- Custom Dataset 생성
- PyTorch 제공 Dataset 사용
1-1) 단순 File 불러오기
data = pd.read_csv("path/to/your/file")
x = torch.from_numpy(data['x'].values).unsqueeze(dim=1).float
y = torch.from_numpy(data['y'].values).unsqueeze(dim=1).float
1-2) Custom Dataset 생성
- CustomDataSet의 경우 반드시 아래의 3가지 요소가 전부 있어야 함
- Constructor: __init__
- 필요한 변수를 선언하고, 데이터셋의 전처리를 해주는 함수
- Length: __len__
- 총 샘플의 수를 가져오는 함수
- Data, Label for each index: __getitem__
- index 번째 데이터와 라벨을 가져오는 함수
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
class CustomDataset(Dataset):
def __init__(self, csv_file): # 필요한 변수를 선언하고, 데이터셋의 전처리르 해주는 함수
super(CustomDataset, self).__init__()
self.data = pd.read_csv(csv_file)
def __len__(self): # 총 샘플의 수를 가져오는 함수
return len(self.data)
def __getitem__(self, index):
sample = torch.tensor(self.data.iloc[index, 0:3]).int()
label = torch.tensor(self.data.iloc[index, 3]).int() # index값이 4번째 열에 존재하는 경우
return sample, label
tensor_dataset = CustomDataset("path/to/your/file")
dataset = DataLoader(tensor_dataset, batch_size=4, shuffle=True)
1-3) PyTorch 제공 Dataset 생성
import torchvision.transforms as transforms
from torchvision.datasets import MNIST
import requests
mnist_transform = transforms.Compose([
transforms.ToTensor(), # PyTorch tensor type 변환
transforms.Normalize((0.5,), (1.0,)) # mean = 0.5, std = 1.0이 되도록 데이터 분포를 정규화
])
download_root = "path/you/want/to/save"
train_dataset = MNIST(download=download_root, transform=mnist_transform, train=True, download=True)
valid_dataset = MNIST(download=download_root, transform=mnist_transform, train=False, download=True)
test_dataset = MNIST(download=download_root, transform=mnist_transform, train=False, download=True)
2) DataLoader 생성
- DataLoader 객체는 Train/Validation/Test 에 사용될 데이터 전체를 보관
- Train/Validation/Test시 batch_size만큼 Data를 꺼내서 사용
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=16, shuffle=False)
- batch_size: Dataset의 전체 데이터가 batch_size로 slice 되어 공급됨
- shuffle: 매 epoch마다 Dataset을 섞어, 데이터가 학습되는 순서를 바꾸는 기능 (학습을 할 때는 True로 설정하는 것을 권장)
- num_worker: 동시에 처리하는 processor의 수 (num_worker 하나를 더 추가하면 20% 정도 속도가 빨라진다)
Q. 왜 val_loader, test_loader의 shuffle parameter가 False인가?
A. Validation, test Data에 대해 Shuffle을 한다고 해서 accuracy나 loss가 바뀌지는 않기 때문이다.
accuracy의 경우 맞은 개수를 세는 순서가 다르다고 해서 그 개수가 달라질 수 없다.
loss도 마찬가지로 각 batch의 loss의 순서가 다르다고 해서 총 Loss가 달라질 수 없다.
Training Data에 대해 Shuffle을 하는 이유는 각 epoch마다 같은 순서의 데이터에 대해 학습하지 않도록 하기 위함이다. (가짜 패턴을 학습하지 않기 위해서)
Yes, shuffling the validation/test data will not have any impact on the accuracy, loss etc.
Shuffling is done during the training to make sure we aren’t exposing our model to the same cycle (order) of data in every epoch. It is basically done to ensure the model isn’t adapting its learning to any kind of spurious pattern.
3) Model Define
- Basic Neural Network
- nn.Module Inheritance
- Sequential Neural Network
3-1) Basic Neural Network
- nn.Module를 상속받지 않는 매우 단순한 모델
model = nn.Linear(n_features=1, out_features=1, bias=True)
Q. nn.Module은 무엇이길래 대부분의 Neural Network 구축시 Class로 상속받는가?
A. 모든 Neural Network의 근간이 되는 기본 Class로, forward, backward 등의 함수를 포함하고 있다.
3-2) nn.Module( ) 상속
- nn.Module을 상속 받기 때문에 기본적으로 __init__( )과 forward( ) 함수를 사용할 수 있음
- __Init__: 모델에서 사용될 module, activation function 등을 정의
- forward( ): 모델에서 실행되어야 하는 연산을 정의
import torch.nn as nn
import torch.nn.functional as F
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__() # nn.Module을 상속받는다
self.conv1 = nn.Conv2d(1, 20, 5)
self.conv2 = nn.Conv2d(20, 20, 5)
def forward(self, x): # 어떻게 forward propagation을 할건지 정의
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
return x
3-3) Sequential Neural Network 이용
- Sequential 객체 안에 포함된 각 Module을 순차적으로 실행해줌
- 가독성이 뛰어나게 코드로 작성할 수 있음
- nn.Sequentia은 모델의 계층이 복잡할수록 효과가 더 뛰어나다
Q. nn.ReLU vs F.relu의 차이
A. nn.ReLU는 nn.Sequential Model에 추가할 수 있는 nn.Module을 만들고, F.relu는 forward( )에 있는 함수처럼 쓸 수 있다.
nn.ReLU() creates an nn.Module which you can add e.g. to an nn.Sequential model.
nn.functional.relu on the other side is just the functional API call to the relu function, so that you can add it e.g. in your forward method yourself.
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.layer = nn.Sequential(
nn.Conv2d(1, 20, 5),
nn.ReLU(),
nn.Conv2d(20, 64, 5),
nn.ReLU(),
)
def forward(self, x):
return self.layer(x)
+α) nn.Sequential을 사용한 vs nn.Sequential을 사용하지 않은 Neural Network
- nn.Sequential을 사용하지 않은 Neural Network
class MyNeuralNetwork(nn.Module):
def __init__(self):
super(MyNeuralNetwork, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=64, out_channels=30, kernel_size=5)
self.fc1 = nn.Linear(in_features=30 * 5 * 5, out_features=128, bias=True)
self.fc2 = nn.Linear(in_features=128, out_features=10, bias=True)
def forward(self, x):
x = F.relu(self.conv1(x), inplace=True)
x = F.max_pool2d(x, (2, 2))
x = F.relu(self.conv2(x), inplace=True)
x = F.max_pool2d(x, (2, 2))
x = x.view(x.shape[0], -1) # 2-Dimension 으로 변환
x = F.relu(self.fc1(x), inplace=True)
x = F.relu(self.fc2(x), inplace=True)
return x
Q. x = x.view(x.shape[0], -1)가 무엇을 의미하는가?
A. 각 Sample을 구분하기 위해 Batch 차원을 유지하면서, Batch내의 sample들을 1차원 vector로 flatten 시킨다.
일반적으로 CNN Input data의 Tensor: [Batch Size, # of Channels, Height, Width] 의 shape을 가진다.
Neural Network가 동일한 연산을 Batch에 있는 모든 sample에 독립적으로 적용하기 때문에,
모든 sample을 유지하면서 다른 차원을 변형하고자 할 때 유용하고자 할 때 x.shape[0]을 사용하여 tensor를 재구성하면 유리하다.
- nn.Sequential을 사용한 Neural Network
class MyNeuralNetwork(nn.Module):
def __init__(self):
super(MyNeuralNetwork, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(3, 64, 5),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
)
self.layer2 = nn.Sequential(
nn.Conv2d(64, 30, 5),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
)
self.layer3 = nn.Sequential(
nn.Linear(30 * 5 *5, 128, bias=True),
nn.ReLU(inplace=True)
)
self.layer4 = nn.Sequential(
nn.Linear(128, 10, bias=True),
nn.ReLU(inplace=True)
)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = x.view(x.shape[0], -1)
x = self.layer3(x)
x = self.layer4(x)
return x
Q. nn.ReLU(inplace=True)가 무엇을 의미하는가?
A. 들어온 Input값이 없어지고 output 값만 남는다고 볼 수 있다.
따라서 기존에 input값을 저장하던 메모리를 따로 할당할 필요가 없기 때문에 메모리측면에서 이득을 얻을 수 있다.
즉, inplace=True로 한다면 input값에 대한 output을 따로 저장하는 것이 아니라 output이 그 자리에 있던 데이터 값을 대신한다.
x = self.layer1(x)
x = self.activation(x)
x = self.layer2(x)
4) Model Training
model.train( )
train_loss, correct, total_samples = 0.0, 0, 0
for batch_X, batch_y in train_loader:
- batch_X, batch_y = batch_X.to(device), batch_y.to(device)
- outputs = model(batch_X)
- loss = loss_function(outputs, batch_y)
- train_loss += loss.item( )
- _, predicted = torch.max(outputs.data, 1) or predicted = torch.argmax(outputs.data, 1)
- total_samples += batch_y.size(0)
- correct += (predicted == batch_y).sum( ).item( )
- optimizer.zero_grad( )
- loss.backward( )
- optimizer.step( )
train_loss /= total_samples
train_accuracy = 100 * (correct / total_samples)
4-1) Model, Loss Function, Optimizer, Scheduler 정의
- Optimizer: Data, Loss Function을 바탕으로 Model의 update 방법을 결정
- step( ): 전달받은 Parameter를 업데이트
- zero_grad( ): Optimizer에 사용된 Parameter들의 Gradient을 0으로 만들어준다
- torch.optim.lr_scheduler: epoch에 따라 learning_rate를 조정하게 해준다
Scheduler
- 미리 지정한 횟수의 epoch를 지날 때마다 학습률을 감소시킨다
- 학습률 Scheduler를 이용하면 학습 초기에는 빠른 학습을 진행하다가 Global Minimum 근처에 도달하면 learning_rate를 줄여서 Optimal Point를 찾아갈 수 있도록 해준다
Q. optimizer.zero_grad( )를 해주는 이유는 뭘까?
A. PyTorch는 loss.backward( )를 호출할 때마다 Gradient값들을 누적해서 더해주기 때문이다.
따라서 학습 loop를 돌때 이상적으로 학습이 이루어지기 위해서는
한번의 학습이 완료되어지면(즉, Iteration이 한번 끝나면) gradients를 항상 0으로 만들어 주어야 한다.
즉 전방향 학습, Paramter들의 Gradient 초기화를 위해서이다.
단, RNN을 구현할 때는 누적하여 계산하는 것이 오히려 효과적이다.
6. Train, Validation, Test Logic
Train, Validation, Test 의 모든 과정들은 One Epoch 안에서 batch size만큼 돌아간다
Train Process
model.train( )
train_loss, correct, total_samples = 0.0, 0, 0
for batch_X, batch_y in train_loader:
- batch_X, batch_y = batch_X.to(device), batch_y.to(device)
- outputs = model(batch_X)
- loss = loss_function(outputs, batch_y)
- train_loss += loss.item( )
- _, predicted = torch.max(outputs, 1) or predicted = torch.argmax(outputs, 1)
- total_samples += batch_y.size(0)
- correct += (predicted == batch_y).sum( ).item( )
- optimizer.zero_grad( )
- loss.backward( )
- optimizer.step( )
train_loss /= total_samples
train_accuracy = 100 * (correct / total_samples)
Only Train Process에 들어가는 코드는 Green 처리함
def train(model, train_loader, optimizer, criterion):
model.train()
train_loss, correct, total = 0.0, 0, 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
train_loss += loss.item()
correct += (labels == torch.argmax(outputs, dim=1)).sum().item()
total += labels.size(0)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss /= total
train_accruacy = 100 * (correct / total)
return train_loss, train_accruacy
Q. Multi-Class Classification의 경우 위와 같이 코드를 작성하면 되지만, Binary Classification의 경우는 어떻게 다르게 작성해야 되는가?
A. Multi-Class Classification의 경우 다음과 같이 코드가 작성된다:
loss_function = nn.CrossEntropyLoss() # nn.Softmax()을 이미 포함하고 있음
# case 1
predicted = torch.argmax(outputs, dim=1)
# case 2
_, predicted = torch.max(outputs, dim=1)
correct = (batch_y == predicted).sum().item()
Binary Classification의 경우 다음과 같이 코드가 작성된다:
loss_function = nn.BCEWithLogitsLoss() # nn.Sigmoid()를 이미 포함하고 있음
predicted = torch.sigmoid(outputs) >= 0.5 # 시그모이드 함수를 적용하고 0.5 기준으로 예측 생성
correct += (batch_y == predicted).sum().item()
Q. Train, Validation, Test Process의 코드 뒤에 들어가는 .item method는 어떠한 기능을 하니?
A. '.item()' 메소드를 사용하는 이유는 tensor에서 파이썬 숫자 타입의 값을 가져오기 위해서이다. '.sum()' 메소드는 텐서에 있는 모든 값을 더하여 새로운 텐서를 반환한다. 이 반환된 텐서는 여전히 PyTorch 텐서이며, GPU나 다른 텐서 연산을 위한 메모리에 존재할 수 있다.
correct += (predicted == batch_y).sum().item()에서 .sum()은 예측이 정확한지의 여부를 나타내는 불리언 텐서에 적용되어, 정확한 예측의 총 개수를 하나의 스칼라 텐서로 더한다. 그 후에 .item()은 이 스칼라 텐서를 파이썬의 int 타입 값으로 변환한다. 이렇게 변환된 값은 correct 변수에 누적되어 총합을 구할 수 있게 한다.
.item()을 사용하지 않고 .sum()만을 사용할 경우, correct는 스칼라 값을 갖는 텐서로 취급되며, 이후에 다른 파이썬 숫자 타입과 함께 연산될 때 자동으로 파이썬 타입으로 변환된다.
그러나 이 변환은 자동으로 이루어지지 않는 경우가 많기 때문에 .item()을 사용하여 명시적으로 타입을 변환하는 것이 좋다.
그러므로, 코드에서 .item()을 생략하면 에러는 발생하지 않지만, 보통은 정확도와 같은 값을 파이썬의 숫자 타입으로 관리하고 싶을 때 .item()을 사용하여 명시적으로 변환하는 것이 좋다. 이렇게 함으로써, 그 값을 다른 파이썬 기본 타입과 쉽게 비교하고 연산할 수 있다.
Validation Process
model.eval( )
val_loss, correct, total = 0.0, 0, 0
with torch.no_grad( ):
for batch_X_val, batch_y_val in val_loader:
- batch_X_val, batch_y_val = batch_X_val.to(device), batch_y_val.to(device)
- outputs = model(batch_X_val)
- loss = loss_function(outputs, batch_y_val)
- val_loss += loss.item( )
- _, predicted = torch.max(outputs, 1)
- total += batch_y_val.size(0)
- correct += (predicted == batch_y_val).sum( ).item( )
val_loss /= total
val_accuracy = 100 * (correct / total)
def validate(model, val_loader, criterion):
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
val_loss /= total
val_accuracy = 100 * correct / total
return val_loss, val_accuracy
Test Process
model.eval( )
correct, total = 0, 0
with torch.no_grad( ):
for batch_X_test, batch_y_test in test_loader:
- batch_X_test, batch_y_test = batch_X_test.to(device), batch_y_test.to(device)
- outputs = model(batch_X_test)
- loss = loss_function(outputs, labels)
- _, predicted = torch.max(outputs, 1)
- total += batch_y_test.size(0)
- correct += (predicted == batch_y_test).sum( ).item( )
- test_loss += loss.item()
test_loss /= total
test_accuracy = 100 * (correct / total)
def test(model, test_loader):
model.eval()
correct, total, test_loss = 0, 0, 0.0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
correct += (torch.argmax(outputs, 1) == labels).sum().item()
total += labels.size(0)
test_loss += loss.item()
test_loss /= total
test_accuracy = (correct / total) * 100
return test_loss, test_accuracy
train_losses, train_accuracies, val_losses, val_accuracies = [], [], [], []
# 훈련 및 검증
epochs = 50
cnt = 0
for epoch in range(epochs):
train_loss, train_accuracy = train(model, train_loader, optimizer, criterion)
val_loss, val_accuracy = validate(model, val_loader, criterion)
train_losses.append(train_loss)
train_accuracies.append(train_accuracy)
val_losses.append(val_loss)
val_accuracies.append(val_accuracy)
print(
f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%, Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.2f}%"
)
cnt += 1
# Early Stopping 호출
early_stopping(val_loss, model)
if early_stopping.early_stop:
print("Early stopping")
break
# 테스트 세트에서의 성능
test_loss, test_accuracy = test(model, test_loader)
print(f"Test Accuracy: {test_accuracy:.2f}%")
7. PyTorch - Code Implementation
7-1) Data Analysis
- price: 자동차 가격
- maint: 자동차 유지 비용
- doors: 자동차 문 개수
- persons: 수용 인원
- lug_capacity: 수하물 용량
- safety: 안전성
- output: 차 상태
- unacc: 허용 불가능한 수준
- acc: 허용 가능한 수준
- good: 양호
- vgood: 매우 좋은
6개의 column을 이용하여 output (차 상태) column을 예측하는 Task
7-2) 라이브러리, 데이터 호출
- 라이브러리 호출
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
- 데이터 호출
dataset = pd.read_csv("./car_evaluation.csv")
dataset.head()
Categorical Variable(Column)들을 Numerical Variable(Column)으로 변환해줘야 함
- Dataset의 모든 Column들이 Obejct Datatype
dataset.dtypes
price object
maint object
doors object
persons object
lug_capacity object
safety object
output object
dtype: object
Q. Pandas에서 Object dtype과 Category dtype이 어떻게 다른가?
A. 일반적인 문자열을 갖는 칼럼은 object로 사용하고, 값이 종류가 제한적(fixed)일 때 category를 사용하면 된다.
object
판다스에서는 문자열을 object라는 자료형으로 나타낸다. 파이썬에서는 문자열을 string이라고 하지만, 판다스는 object라고 한다. pd.DataFrame을 사용하여 데이터프레임을 만들때 dtype(형식)을 지정해주는게 아니라면 일반적으로 데이터를 받아들일 때 숫자형을 제외한 나머지는 object로 받아들이는 경향이 있다.
category
category 형식은 가능한 값들의 범위가 고정되어있고, 한정적일 때 매우 사용한다고 한다. 공식문서에 따르만 다음과 같은 경우에 사용한다.
A string variable consisting of only a few different values. Converting such a string variable to a categorical variable will save some memory, see here.
The lexical order of a variable is not the same as the logical order (“one”, “two”, “three”). By converting to a categorical and specifying an order on the categories, sorting and min/max will use the logical order instead of the lexical order, see here.
As a signal to other Python libraries that this column should be treated as a categorical variable (e.g. to use suitable statistical methods or plot types).
요약하자면 일반적인 string을 갖는 칼럼은 object로 사용하고, 값이 종류가 제한적일 때 category를 사용하면 된다.
데이터 형식에 따라 가질 수 있는 값의 범위가 다르고, 메모리 사용공간에도 차이가 있다. 불필요한 메모리 사용량을 줄이기 위해 데이터를 잘 파악하고 알맞은 형식을 지정해주는 습관을 들이면 좋다.
- 데이터(Target Value, output column) 분포 형태 시각화
fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 8
fig_size[1] = 6
plt.rcParams["figure.figsize"] = fig_size
dataset["output"].value_counts().plot(
kind="pie",
autopct="%0.05f%%",
colors=["lightblue", "lightgreen", "orange", "pink"],
explode=(0.05, 0.05, 0.05, 0.05),
)
7-3) Preprocessing
- Dataset의 column들을 Obejct -> Category Datatype으로 변환
# dataset columns들의 list
categorical_columns = [columns for columns in dataset.columns if columns != "output"]
# astype() method을 이용하여 data를 categorical value로 변환
for column in categorical_columns:
dataset[column] = dataset[column].astype("category")
dataset.dtypes
price category
maint category
doors category
persons category
lug_capacity category
safety category
output object
dtype: object
tensor로 변환하는 과정에서 필자는 PPT에 적힌대로 하지 않고, 바로 for문을 이용해 각 Series에 접근하여 Encoding하는 방식을 택했다.
- Category dtype을 tensor로 변환
# dataset columns들의 list
categorical_columns = [columns for columns in dataset.columns if columns != "output"]
# astype() method을 이용하여 각 Column를 categorical value로 변환
# .cat.codes를 이용하여 정수형으로 Encoding (각 Series 반환)
for column in categorical_columns:
dataset[column] = dataset[column].astype("category").cat.codes
dataset.dtypes
price int8
maint int8
doors int8
persons int8
lug_capacity int8
safety int8
output object
dtype: object
Q. cat.codes.values는 무엇을 의미하는가?
A. 'category' dtype인 Series에 대해 cat 속성을 이용하여 Categorical 객체를 만들고,
이를 통해 Categorical Method인 codes, categories에 쉽게 접근할 수 있다.
즉, Pandas에는 Categorical dtype으로 정수 기반의 Category dtype의 데이터로 Encoding할 수 있다.
- category dtype인 Series의 values 속성이 Categorical 객체
type(dataset['price'].values)
pandas.core.arrays.categorical.Categorical
- codes 속성을 사용하여 Series 반환
dataset['price'].cat.codes
0 3
1 3
2 3
3 3
4 3
..
1723 1
1724 1
1725 1
1726 1
1727 1
Length: 1728, dtype: int8
- codes 속성을 사용하여 ndarray를 반환하는 2가지 방법
dataset['price'].cat.codes.values
dataset['price'].values.codes
array([3, 3, 3, ..., 1, 1, 1], dtype=int8)
- categories 속성을 사용하는 2가지 방법
dataset['price'].cat.categories
dataset['price'].values.categories
Index(['high', 'low', 'med', 'vhigh'], dtype='object')
categorical_data = dataset.drop(['output'], axis=1).values
categorical_data
array([[3, 3, 0, 0, 2, 1],
[3, 3, 0, 0, 2, 2],
[3, 3, 0, 0, 2, 0],
[3, 3, 0, 0, 1, 1],
[3, 3, 0, 0, 1, 2],
[3, 3, 0, 0, 1, 0],
[3, 3, 0, 0, 0, 1],
[3, 3, 0, 0, 0, 2],
[3, 3, 0, 0, 0, 0],
[3, 3, 0, 1, 2, 1]], dtype=int8)
categorical_data = torch.tensor(categorical_data, dtype=torch.int64)
categorical_data[:10]
tensor([[3, 3, 0, 0, 2, 1],
[3, 3, 0, 0, 2, 2],
[3, 3, 0, 0, 2, 0],
[3, 3, 0, 0, 1, 1],
[3, 3, 0, 0, 1, 2],
[3, 3, 0, 0, 1, 0],
[3, 3, 0, 0, 0, 1],
[3, 3, 0, 0, 0, 2],
[3, 3, 0, 0, 0, 0],
[3, 3, 0, 1, 2, 1]])
outputs = pd.get_dummies(dataset["output"]).values
print(outputs.shape)
outputs = torch.tensor(outputs).flatten()
print(outputs.shape)
print(outputs)
(1728, 4)
torch.Size([6912])
tensor([False, False, True, ..., False, False, True])
print(categorical_data.shape)
print(outputs.shape)
torch.Size([1728, 6])
torch.Size([6912])
7-4) Categorical Column의 Embedding 크기 정의
- 차원의 크기는 주로 Column의 고유값 수(Category 개수)를 2로 나눈다
# 각 Column별 Category의 개수
categorical_column_sizes = [len(set(dataset[column])) for column in categorical_columns]
# 각 Column별 (Category 개수, 차원의 크기)
categorical_embedding_sizes = [
(col_size, min(50, (col_size + 1) // 2)) for col_size in categorical_column_sizes
]
print(categorical_column_sizes)
print(categorical_embedding_sizes)
[4, 4, 4, 3, 3, 3]
[(4, 2), (4, 2), (4, 2), (3, 2), (3, 2), (3, 2)]
7-5) Dataset Split
- Dataset을 Train, Test Dataset으로 분리
total_records = 1728 # total dataset의 총 record 수 (row 개수)
test_records = int(total_records * 0.2) # 전체 dataset의 20%를 test data로 사용
# Features
categorical_train_data = categorical_data[: (total_records - test_records)]
categorical_test_data = categorical_data[(total_records - test_records) : total_records]
# Target Values
train_outputs = outputs[: (total_records - test_records)]
test_outputs = outputs[(total_records - test_records) : total_records]
print(len(categorical_train_data))
print(len(categorical_test_data))
print(len(train_outputs))
print(len(test_outputs))
1383
345
1383
345
7-6) Model Network 형성
def __init__(self, embedding_size, output_size, layers, p)
- self: Instance 자기 자신을 의미
- embedding_size: Categorical Variable의 Embedding size
- output_size: Output Layer의 크기
- layers: 모든 Layer에 대한 목록
- p: Dropout (기본값은 0.5)
class Model(nn.Module):
def __init__(self, embedding_size, output_size, layers, p=0.4):
super(Model, self).__init__()
self.all_embeddings = nn.ModuleList(
[nn.Embedding(ni, nf) for ni, nf in embedding_size]
)
self.embedding_dropout = nn.Dropout(p)
all_layers = []
num_categorical_cols = sum((nf for ni, nf in embedding_size))
input_size = num_categorical_cols
for i in layers:
all_layers.append(nn.Linear(input_size, i))
all_layers.append(nn.ReLU(inplace=True))
all_layers.append(nn.BatchNorm1d(i))
all_layers.append(nn.Dropout(p))
input_size = i
all_layers.apend(nn.Linear(layers[-1], output_size))
self.layers = nn.Sequential(*all_layers)
- Linear: Weight Matrix와 Input Feature 간의 Linear Combination(선형 변환)을 진행한 결과
- ReLU: Activation Function으로서 사용
- BatchNorm1d: Batch Normalization으로 사용 (Neural Network 안에서 Data mean, std를 조정하는 것)
- Dropout: Overfitting 방지를 위해서 모델 학습 시 일부 뉴런을 turn-off 하면서 학습하는 방법
- forward( ): 함수 데이터를 입력받아 연산을 진행, Model 객체를 데이터와 함께 호출하면 자동으로 실행됨
class Model(nn.Module):
def forward(self, x_categorical):
embeddings = []
for i, e in enumerate(self.all_embeddings):
embeddings.append(e(x_categorical[:, i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)
x = self.layers(x)
return x
- Final Code
class Model(nn.Module):
def __init__(self, embedding_size, output_size, layers, p=0.4):
super(Model, self).__init__()
self.all_embeddings = nn.ModuleList(
[nn.Embedding(ni, nf) for ni, nf in embedding_size]
)
self.embedding_dropout = nn.Dropout(p)
all_layers = []
num_categorical_cols = sum((nf for ni, nf in embedding_size))
input_size = num_categorical_cols
for i in layers:
all_layers.append(nn.Linear(input_size, i))
all_layers.append(nn.ReLU(inplace=True))
all_layers.append(nn.BatchNorm1d(i))
all_layers.append(nn.Dropout(p))
input_size = i
all_layers.apend(nn.Linear(layers[-1], output_size))
self.layers = nn.Sequential(*all_layers)
def forward(self, x_categorical):
embeddings = []
for i, e in enumerate(self.all_embeddings):
embeddings.append(e(x_categorical[:, i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)
x = self.layers(x)
return x
7-7) Model Class Object, Loss Function, Optimizer 생성
model = Model(categorical_embedding_sizes, 4, [200, 100, 50], p=0.4)
print(model)
loss_function = nn.CrossEntropyLoss() # Cross - Entropy Loss Function 사용
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Optimizer으로 Adam 사용
Model(
(all_embeddings): ModuleList(
(0-2): 3 x Embedding(4, 2)
(3-5): 3 x Embedding(3, 2)
)
(embedding_dropout): Dropout(p=0.4, inplace=False)
(layers): Sequential(
(0): Linear(in_features=12, out_features=200, bias=True)
(1): ReLU(inplace=True)
(2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.4, inplace=False)
(4): Linear(in_features=200, out_features=100, bias=True)
(5): ReLU(inplace=True)
(6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): Dropout(p=0.4, inplace=False)
(8): Linear(in_features=100, out_features=50, bias=True)
(9): ReLU(inplace=True)
(10): BatchNorm1d(50, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(11): Dropout(p=0.4, inplace=False)
(12): Linear(in_features=50, out_features=4, bias=True)
)
)
- Cross-Entropy: Multi-Class Classification에서 사용
- 모델의 예측이 실제 Label과 얼마나 일치하는지를 측정
- Model이 높은 확률로 올바른 Class를 예측할 때 손실이 감소하며, 모델이 틀린 예측을 할 때 손실이 증가
7-8) Model Training
epochs = 500 # 전체 데이터셋을 몇 번 반복하여 모델을 훈련시킬 것인지 결정
aggregated_losses = [] # 각 epoch에서의 손실을 저장하는 list
# train data의 label을 model의 device로 이동시킴
train_outputs = train_outputs.to(device=device, dtype=torch.int64)
for i in range(
1, epochs + 1
): # for문은 500회 반복되고 각 반복마다 loss function이 오차를 계산
outputs = model(categorical_train_data).to(
device
) # 현재 모델을 사용하여 훈련 데이터의 예측값을 계산
single_loss = loss_function(outputs, train_outputs)
aggregated_losses.append(single_loss)
if i % 25 == 1:
print(f"epoch: {i:3}, Loss: {single_loss.item():10.8f}")
optimizer.zero_grad() # optimizer의 기울기를 초기화
single_loss.backward() # 가중치를 계산하기 위해 loss function의 backward() method 호출
optimizer.step() # optimizer의 step() method를 이용하여 기울기 업데이트
print(f"epoch: {i:3}, Loss: {single_loss.item():10.10f}")
epoch: 1, Loss: 1.61841047
epoch: 26, Loss: 1.44102693
epoch: 51, Loss: 1.33527386
epoch: 76, Loss: 1.21690285
epoch: 101, Loss: 1.07019496
epoch: 126, Loss: 0.94250494
epoch: 151, Loss: 0.85144371
epoch: 176, Loss: 0.74453878
epoch: 201, Loss: 0.69113714
epoch: 226, Loss: 0.66099828
epoch: 251, Loss: 0.66160893
epoch: 276, Loss: 0.61902350
epoch: 301, Loss: 0.60708290
epoch: 326, Loss: 0.59607738
epoch: 351, Loss: 0.59685850
epoch: 376, Loss: 0.59025848
epoch: 401, Loss: 0.58866191
epoch: 426, Loss: 0.58108068
epoch: 451, Loss: 0.57939297
epoch: 476, Loss: 0.57848954
epoch: 500, Loss: 0.5751152635
7-9) Model Test Score 예측
# test data의 label을 model의 device로 이동시킴
test_outputs = test_outputs.to(device=device, dtype=torch.int64)
# 연산 추적을 비활성화
with torch.no_grad():
# test data를 모델에 전달하여 예측값을 계산
outputs = model(categorical_test_data).to(device) # 예측값은 y_val에 저장됨
loss = loss_function(
outputs, test_outputs
) # 계산된 예측값과 실제 test label 간의 loss를 계산
print(f"Loss: {loss:.8f}")
Loss: 0.57596338
7-10) Model Evaluation
# 예측값에서 가장 높은 확률을 가진 클래스 인덱스를 선택
predicted_classes = np.argmax(outputs, axis=1)
print(confusion_matrix(test_outputs, predicted_classes))
print(classification_report(test_outputs, predicted_classes))
print(accuracy_score(test_outputs, predicted_classes))
[[258 1]
[ 86 0]]
precision recall f1-score support
0 0.75 1.00 0.86 259
1 0.00 0.00 0.00 86
accuracy 0.75 345
macro avg 0.38 0.50 0.43 345
weighted avg 0.56 0.75 0.64 345
0.7478260869565218