출처 및 코드 : https://youtu.be/U0s0f995w14

 

Transformer 인코더 만들기


 먼저 워드 임베딩을 통해 단어를 256의 임베딩 차원으로 나타내고, 트렌스포머의 멀티 헤드 어텐션 헤드의 개수를 8로 설정하면 각 헤드마다의 차원은 256을 8로 나눈 몫인 32차원이다. V, K, Q는 각각 value, key, query값이고 만약, N = 10, embed_size = 256, src_vocab_size = 7 라고 하면, 10개의 문장이 임베딩 레이어를 거쳐 (10, 7, 256)이 되고 8개의 헤드로 쪼개지기 위해 (10, 7, 8, 32)로 reshape한다. 그리고 나서 value, key, query 각각 Linear 레이어를 통과하고 나면 차원은 마찬가지로 (10, 7, 8, 32)이다. 

 이제 어텐션을 구하기 위해서 QK^t를 torch.einsum으로 행렬곱하여 구하고 임베딩 크기의 제곱근 한 값으로 나눠 주어 softmax를 취한다. 이 값에 마찬가지로 torch.einsum을 이용하여 V를 곱하면 8개의 head마다 attention이 구해진다. 이것을 다시 rshape으로 concat해주고 마지막 Linear 레이어를 통과시킨다.

import torch
import torch.nn as nn

class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size # 256
        self.heads = heads # 8
        self.head_dim = embed_size // heads # 32

        assert (self.head_dim * heads == embed_size), "Embed size needs to be div by heads"

        self.values = nn.Linear(self.head_dim, self.head_dim, bias=False) # 32 -> 32
        self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False) # 32 -> 32
        self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)  # 32 -> 32
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size) # 8 * 32 = 256 -> 256

    def forward(self, values, keys, query, mask):
        N = query.shape[0]
        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]

        # Split embedding into self.heads pieces
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        queries = query.reshape(N, query_len, self.heads, self.head_dim)

        values = self.values(values)
        keys = self.keys(keys)
        queries = self.queries(queries)

        energy = torch.einsum("nqhd, nkhd -> nhqk", [queries, keys]) # QK^t
        # queries shape : (N, query_len, heads, head_dim)
        # keys shape : (N, key_len, heads, head_dim)
        # energy shape : (N, heads, query_len, key_len)

        if mask is not None:
            energy = energy.masked_fill(mask==0, float("-1e20"))

        attention = torch.softmax(energy / (self.embed_size ** 0.5), dim=3)

        out = torch.einsum('nhql, nlhd -> nqhd', [attention, values]).reshape(N, query_len, self.heads * self.head_dim)
        # attention shape : (N, heads, query_len, key_len)
        # values shape : (N, value_len, heads, head_dim)
        # after einsum (N, query_len, heads, head_dim) then flatten last two dimensions
        out = self.fc_out(out)
        return out

 위에서 만든 파란색 박스친 멀티 헤드 셀프 어텐션 부분은 가져다 쓴다. value, key query가 어텐션 레이어로 들어가고 이때 query는 residual connection이 들어가는데 어텐션을 통과한 값과 query를 더하여 LayerNormalization해준다. 다음으로 forward_expansion배 만큼 커졌다가 다시 줄어드는 Feed Forward 레이어를 통과하고 그 전의 값과 더하여 두 번째 LayerNormalization 해준다. TransformerBolck 모듈이 완성되었다.

class TransformerBlock(nn.Module):
    def __init__(self, embed_size, heads, dropout, forward_expansion):
        super(TransformerBlock, self).__init__()
        self.attention = SelfAttention(embed_size, heads)
        self.norm1 = nn.LayerNorm(embed_size)
        self.norm2 = nn.LayerNorm(embed_size)

        self.feed_forward = nn.Sequential(
            nn.Linear(embed_size, forward_expansion * embed_size),
            nn.ReLU(),
            nn.Linear(forward_expansion * embed_size, embed_size)
        )
        self.dropout = nn.Dropout(dropout)

    def forward(self, value, key, query, mask):
        attention = self.attention(value, key, query, mask)

        x = self.dropout(self.norm1(attention + query))
        forward = self.feed_forward(x)
        out = self.dropout(self.norm2(forward + x))
        return out

위에서 TransformerBolck 모듈을 만들었으니 이제 인코더 부분을 완성할 차례이다. Transformer는 RNN을 사용하지 않기 때문에 순서에 대한 정보를 주기 위하여 위치 인코딩(Positional Encoding) 과정을 거친다. 먼저 input으로 문장이 들어오고 워드 임베딩 레이어를 거친다. 이 때 단어의 순서를 나타내기 위하여 1부터 단어길이까지 arange하고 문장개수 만큼 복사한다. 그 다음 임베딩 레이어에 통과 시켜 위치 인코딩을 한다. 논문에서는 사인과 코사인 함수를 이용하여 했지만 이렇게 해도 잘 동작한다고 한다. 이제 워드 임베딩 레이어를 통과한 값과 위치 인코딩 레이어를 통과한 값을 더하여 TrnasformerBolck에 넘겨줄 value, key query로 사용한다. 그리고 이 TrnasformerBlock은 위로 N번 만큼 반복하여 통과 시킬수 있다.

 

class Encoder(nn.Module):
    def __init__(self, src_vocab_size, embed_size, num_layers, heads, device,
                 forward_expansion, dropout, max_len):
        super(Encoder, self).__init__()
        self.embed_size = embed_size
        self.device = device
        self.word_embedding = nn.Embedding(src_vocab_size, embed_size)
        self.positional_embedding = nn.Embedding(max_len, embed_size)

        self.layers = nn.ModuleList([
            TransformerBlock(embed_size, heads, dropout=dropout, forward_expansion=forward_expansion)
            for _ in range(num_layers)
        ])
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask):
        N, seq_length = x.shape
        positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device)

        out = self.dropout(self.word_embedding(x) + self.positional_embedding(positions))

        for layer in self.layers:
            out = layer(out, out, out, mask) # value, key, query
        return out

Transformer 디코더 만들기


타겟 문장에서 임베딩 레이어와 위치 인코딩을 거쳐나온 값 x가 디코더의 SelfAttention의 value, key, query로 들어가고 mask도 들어간다. attention 모듈을 통과한 값을 다시 x와 더해 LayerNormalization 해주고 이 값을 다시 파란색 박스에 들어갈 query로 사용한다. 이때 인코더로부터 value와 key를 넘겨 받는다. 준비된 value, key, query를 위에서만든 TransformerBolck에 통과시킨다.

class DecoderBlock(nn.Module):
    def __init__(self, embed_size, heads, forward_expansion, dropout, device):
        super(DecoderBlock, self).__init__()
        self.attention = SelfAttention(embed_size, heads)
        self.norm = nn.LayerNorm(embed_size)
        self.transformer_block = TransformerBlock(embed_size, heads, dropout, forward_expansion)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, value, key, src_mask, trg_mask):
        attention = self.attention(x, x, x, trg_mask)
        query = self.dropout(self.norm(attention + x))
        out = self.transformer_block(value, key, query, src_mask)
        return out

 


타겟 문장에 대해서도 워드 임베딩과 위치 인코딩을 진행하고 DecoderBlock을 N회 반복하여 통과 시킨다. 그런다음 마지막으로 Linear 레이어를 통과시킨다.

class Decoder(nn.Module):
    def __init__(self, trg_vocab_size, embed_size, num_layers, heads, forward_expansion, dropout, device, max_lenght):
        super(Decoder, self).__init__()
        self.device = device
        self.word_embedding = nn.Embedding(trg_vocab_size, embed_size)
        self.position_embedding = nn.Embedding(max_lenght, embed_size)

        self.layers = nn.ModuleList([
            DecoderBlock(embed_size, heads, forward_expansion, dropout, device)
            for _ in range(num_layers)
        ])

        self.fc_out = nn.Linear(embed_size, trg_vocab_size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, enc_out, src_mask, trg_mask):
        N, seq_length = x.shape
        positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device)
        x = self.dropout(self.word_embedding(x) + self.position_embedding(positions))

        for layer in self.layers:
            x = layer(x, enc_out, enc_out, src_mask, trg_mask)

        out = self.fc_out(x)
        return out

인코더와 디코더를 결합시킨다. 마스크 부분은 잘 모르겠다;

class Transformer(nn.Module):
    def __init__(self, src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx,
                 embed_size=256, num_layers=6, forward_expansion=4, heads=8, dropout=0.,
                 device="cpu", max_length=100):
        super(Transformer, self).__init__()
        self.encoder = Encoder(src_vocab_size,
                               embed_size,
                               num_layers,
                               heads,
                               device,
                               forward_expansion,
                               dropout,
                               max_length)

        self.decoder = Decoder(trg_vocab_size,
                               embed_size,
                               num_layers,
                               heads,
                               forward_expansion,
                               dropout,
                               device,
                               max_length)

        self.src_pad_idx = src_pad_idx
        self.trg_pad_idx = trg_pad_idx
        self.device = device

    def make_src_mask(self, src):
        src_mask = (src != self.src_pad_idx).unsqueeze(1).unsqueeze(2)
        # (N, 1, 1, src_len)
        return src_mask.to(self.device)

    def make_trg_mask(self, trg):
        N, trg_len = trg.shape
        trg_mask = torch.tril(torch.ones((trg_len, trg_len))).expand(N, 1, trg_len, trg_len)
        return trg_mask.to(self.device)

    def forward(self, src, trg):
        src_mask = self.make_src_mask(src)
        trg_mask = self.make_trg_mask(trg)
        enc_src = self.encoder(src, src_mask)
        out = self.decoder(trg, enc_src, src_mask, trg_mask)
        return out

if __name__ == "__main__":
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    x = torch.tensor([[1, 5, 6, 4, 3, 9, 5, 2, 0], [1, 8, 7, 3, 4, 5, 6, 7, 2]]).to(device)
    trg = torch.tensor([[1,7, 4, 3, 5, 9, 2, 0], [1, 5, 6, 2, 4, 7, 6, 2]]).to(device)

    src_pad_idx = 0
    trg_pad_idx = 0
    src_vocab_size = 10
    trg_vocab_size = 10
    model = Transformer(src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx).to(device)
    out = model(x, trg[:, :-1])
    print(out.shape)

'NLP' 카테고리의 다른 글

Seq2seq with Attention 도식화  (0) 2021.05.23

data = 비트코인 1분봉 ohlcv 데이터

볼린저밴드

def bollinger_band(data, w = 20, k = 2):
    data = data.copy()
    mbb = data['close'].rolling(w).mean()
    ubb = mbb + k * data['close'].rolling(w).std()
    lbb = mbb - k * data['close'].rolling(w).std()
    data['width'] = ubb - lbb

    # 볼린저 밴드에서 종가 위치
    data['bollinger_band'] = (data['close'] - lbb) / data['width']

    # 볼린저 밴드 넓이 증가량
    data['bollinger_band_ratio'] = np.zeros(len(data))
    data.loc[1:, 'bollinger_band_ratio']= (data['width'][1:].values - data['width'][:-1].values) / data['width'][:-1].values

    data.drop(['width'], axis = 1, inplace = True)

    return data

MACD

def cal_MACD(data, num_long=12, num_short=26, num_signal=9):
    data = data.copy()
    ema_long = data['close'].ewm(span=num_long, min_periods=num_long - 1).mean()
    ema_short = data['close'].ewm(span=num_short, min_periods=num_short - 1).mean()
    MACD = ema_long - ema_short
    MACD_signal = MACD.ewm(span=num_signal, min_periods=num_signal - 1).mean()
    data['MACD_diff'] = MACD - MACD_signal

    # MACD cross
    data['MACD_cross'] = pd.Series(np.where(data['MACD_diff'] >= 0, 1, -1), index=data.index)
    # 지난 MACD 대비 MACD 비율
    data['MACD_lastMACD_ratio'] = np.zeros(len(data))
    data.loc[1:, 'MACD_lastMACD_ratio'] = (data['MACD_diff'][1:].values - data['MACD_diff'][:-1].values) / data[
                                                                                                               'MACD_diff'][
                                                                                                           :-1].values

    data.drop('MACD_diff', axis=1, inplace=True)
    
    return data

RSI

def cal_RSI(data, period = 9):
    data = data.copy()

    U = np.where(data['close'].diff(1) > 0, data['close'].diff(1), 0)
    D = np.where(data['close'].diff(1) < 0, data['close'].diff(1) * (-1), 0)

    AU = pd.Series(U, index = data.index).rolling(window=period, min_periods=period).mean()
    AD = pd.Series(D, index = data.index).rolling(window=period, min_periods=period).mean()
    RS = AU / AD
    data['RSI'] = 1 - 1 / (1 + RS)

    return data

Stochastic Oscilator

def cal_stochastic_oscillator(data, n = 5):
    data = data.copy()
    size = len(data)
    temp=[]
    for i in range(size):
        if i >= n-1:
            tempUp = data['close'][i] - min(data['low'][i-n+1:i+1])
            tempDown = max(data['high'][i-n+1:i+1]) -  min(data['low'][i-n+1:i+1])
            if tempDown == 0:
                tempDown = 0.001
            temp.append(tempUp / tempDown)
        else:
            temp.append(0) #n보다 작은 초기값은 0 설정
    data['sto_K'] = pd.Series(temp,  index=data.index)
    data['sto_D'] = data['sto_K'].rolling(3).mean()
    return data

OBV

def cal_OBV(data, n=9):
    data = data.copy()
    OBV = []
    OBV.append(data['volume'][0])
    for i in range(1, len(data)):
        if data['close'][i] > data['close'][i - 1]:
            OBV.append(OBV[-1] + data['volume'][i])
        elif data['close'][i] < data['close'][i - 1]:
            OBV.append(OBV[-1] - data['volume'][i])
        else:
            OBV.append(OBV[-1])
    OBV = pd.Series(OBV, index=data.index)
    data['OBV_ewm'] = OBV.ewm(n).mean()

    # OBV signal
    data['OBV_cross'] = pd.Series(np.where(OBV >= data['OBV_ewm'], 1,-1), index = data.index)

    # 지난 OBV_ewm 대비 OBV_ewm 비율
    data['OBV_lastOBV_ratio'] = np.zeros(len(data))
    data.loc[1:, 'OBV_lastOBV_ratio'] = (data['OBV_ewm'][1:].values - data['OBV_ewm'][:-1].values) / data['OBV_ewm'][
                                                                                                     :-1].values

    data.drop('OBV_ewm', axis=1, inplace=True)

    return data

Log Return

def cal_log_return(data):
    data = data.copy()
    data['log_return'] = np.zeros(len(data))
    data['log_return'] = np.log(data['close'] / data['close'].shift(1))
    return data

'기타' 카테고리의 다른 글

비트코인 분봉 데이터 가져오기  (0) 2021.06.17
Anaconda, Jupyter notebook 가상환경 만들기  (0) 2020.06.15
python openslide 설치법  (0) 2020.06.15
import pyupbit
import numpy as np
import pandas as pd
import tqdm
import time
import datetime
import argparse

class MakeDataset:
    def __init__(self, ticker, interval, frm, to = None):
        self.ticker = ticker
        self.interval = interval
        self.frm = frm
        self.to = to
        self.dataset = None
        
    def get_dataset(self):
        self.dataset = self.get_ohlcv_continue(self.ticker, self.interval, self.frm, self.to)

        #print('add variables..')
        #self.dataset = add_variables(self.dataset)

        print('done!')
        
        return self.dataset
    
    def get_ohlcv_continue(self, ticker, interval, frm, to = None):
    
        """
    
        ticker(str) : KRW-BTC
        interval : minute1, minute3, minute5, minute10, minute15, minute60
        frm(str) :
    
        """
    
        if isinstance(frm, str):
            frm = pd.to_datetime(frm).to_pydatetime()
        
        if to is not None:
            if isinstance(to, str):
                to = pd.to_datetime(to).to_pydatetime()
        else:
            to = datetime.datetime.now().replace(microsecond=0)
    
    
        if interval == "minute1":
            count = 60
            date_list = list(pd.date_range(start = frm, end = to, freq = 'H').to_pydatetime())
        elif interval == "minute3":
            count = 60
            date_list = list(pd.date_range(start = frm, end = to, freq = '3H').to_pydatetime())
        elif interval == "minute5":
            count = 60
            date_list = list(pd.date_range(start = frm, end = to, freq = '5H').to_pydatetime())
        elif interval == "minute10":
            count = 144
            date_list = list(pd.date_range(start = frm, end = to, freq = 'D').to_pydatetime())
        elif interval == "minute15":
            count = 96
            date_list = list(pd.date_range(start = frm, end = to, freq = 'D').to_pydatetime())
        elif interval == "minute30":
            count = 48
            date_list = list(pd.date_range(start = frm, end = to, freq = 'D').to_pydatetime())
        elif interval == "minute60":
            count = 24
            date_list = list(pd.date_range(start = frm, end = to, freq = 'D').to_pydatetime())

        dataframes = []
        for date in tqdm.tqdm(date_list[1:]):
            try:
                df = pyupbit.get_ohlcv(ticker, interval, count = count, to = date)
                dataframes.append(df)
                time.sleep(0.1)
            except:
                pass
        
        data = pd.concat(dataframes)
        # 중복 인덱스 제거
        data = data.reset_index().drop_duplicates(subset='index', keep='first').set_index('index')
        data.sort_index(inplace=True)

        return data


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--ticker', type = str, default = 'KRW-ETH')
    parser.add_argument('--interval', type = str, default = 'minute3')
    parser.add_argument('--frm', type = str, default = '2021-01-01 00:00:00')
    parser.add_argument('--to', type = str, default = None)
    args = parser.parse_args()

    print(f'{args.ticker} data is being obtained and processed')
    mk = MakeDataset(args.ticker, args.interval, frm = args.frm, to = args.to)
    data = mk.get_dataset()
    data.to_csv("./data/{}.csv".format(args.ticker))
    print('save completed')

'기타' 카테고리의 다른 글

주식/비트코인 보조지표들 계산 python  (0) 2021.06.17
Anaconda, Jupyter notebook 가상환경 만들기  (0) 2020.06.15
python openslide 설치법  (0) 2020.06.15

Seq2Seq


 Seq2seq는 크게 인코더와 디코더 두 개로 구성된 아키텍처로 인코더는 입력 시퀀스(Sequence)를 순차적으로 입력 받아 순환신경망을 통해 입력 시퀀스의 특징을 반영하는 값을 추출하여 하나의 벡터로 만든다. 이 하나의 벡터를 context Vector라고 한다. 디코더에서는 인코더에서 압축한 context vector를 전송받아 순환신경망의 초기 상태(Initial State)로 활용하여 순차적으로 시퀀스를 출력한다.

 

 

 디코더의 순환신경망 첫 번째 초기 입력으로 시퀀스의 시작을 의미하는 <sos>를 받고 인코더로부터 넘어온 context vector를 첫 번째 셀의 hidden state로 사용하여 다음에 등장할 확률이 높은 단어를 예측한다. 이 예측된 단어는 다음 시점인 두 번째 셀의 입력 값이 되고 또 다시 다음에 등장할 단어를 예측하게 된다.  

 

Seq2Seq2를 활용하여 시퀀스 'I am a student'를 시퀀스 '난 학생입니다'를 출력하는 과정 도식화

 

 한편 인코더의 길이가 길어지면 인코더가 압축하는 context vector가 입력 시쿼스의 정보를 제대로 압축하기 어려워지는 병목현상이 발생한다. 이를 해결하기 위해 Attention 기반 Seq2seq 모델이 연구되었다.

 

 

Seq2seq with Attetion


 기본적인 Seq2seq 모델이 디코딩과정에서 context vector만으로 모든 출력을 생성했다면 Attention기반 Seq2seq는 디코딩 과정에서 각 시퀀스마다 서로 다른 context vector를 사용한다. 먼저 인코더에서 시퀀스를 순차적으로 입력받아 순환신경망을 거쳐 Attention layer로 전달한다. Attention layer에서는 입력 시퀀스의 어떤 단어에 집중할지에 대하여 가중치를 조정하는 방식으로 Context vector를 만들어 디코더에 전달하게 되고, 디코더는 Context vector의 정보를 각 step에서 활용하여 시퀀스를 출력한다.

 

Attention layer에 앞서 순환신경망의 인코더의 각 step의 hidden state(hEt)와 디코더의 hidden state(hDt, 처음에는 인코더의 마지막 hidden state hEj값을 사용)를 통해 Attention score를 계산하고 Softmax함수를 통해 정규화하여 Attension wight를 만든다. 다음으로 인코더의 hidden sate와 Attention weights의 가중 합을 계산하여 가중치가 적용된 context vector를 만든다. 마지막으로 디코더에서 Context vector와 디코더의 hidden state 그리고 이전 디코더의 출력값을 이용해 디코더의 다음 hidden state를 출력한다.

 

i = 디코더의 현재 출력 인덱스

j = 인코더의 현재 입력 인덱스

 

에너지(Energy)

$$ e_{ij}=v_{a}^{T}tanh(W_{a}h_{i-1}^{D}+U_{a}h_{j}^{E}) $$

가중치(Weight)

$$ a_{ij}=\frac{exp(e_{ij})}{\sum_{k}exp(e_{ik})} $$

Context Vector

$$ c_{i}=\sum_{j}a_{ij}h_{j}^{E} $$

 

 

구체적인 과정을 살펴보면, 에너지를 구하기 위하여 인코더의 각 스텝의 모든 hidden state hEj를 Fully Connected layer에 통과시킨다 (Ua*hEj). 다음으로 디코더의 현재 step의 hidden sate를 가져오는데, 초기에는 디코더에서 출력한 값이 없으므로 인코더의 마지막 셀의 hidden state(hETx)를 가져와 Fully Connected layer에 통과시킨다 (Wa*h_i-1). 그 다음 두 값을  더한 후 하이퍼블릭 탄젠트 활성화함수를 통과시키고 다시 Fully Connected layer에 통과 시킨다 (vT*tanh(Wa*h_i-1 + Ua*hEj)). 이렇게 구한 에너지 eij를 softmax 함수를 통해 정규화하여 attention score aij를 구한다. 최종적으로 인코더의 hidden state에 attention score를 곱해 context vector ci를 만들게 된다.

Attention의 context vector는 이처럼 시퀀스에서 더 중요한 부분이라고 생각되는 state에 더 높은 가중치를 주게 되어 예측할 때 더 중요한 부분에 집중하게 된다.

 

'NLP' 카테고리의 다른 글

Transformer  (0) 2021.06.30

 대부분의 상황에서 분류기를 학습할 때, 객체 클래스에 비해 매우 많은 배경 클래스가 존재하는 클래스 불균형(Class Imbalance) 문제를 만나게 된다. 큰 차이의 클래스 불균형은 학습시 교차 엔트로피 손실(Cross Entropy Loss)에 영향을 줘 분류기가 쉽게 다수 클래스를 선택하게 만든다. Focal Loss는 교차 엔트로피 손실 함수를 다수 클래스의 가중치를 줄이도록 재구성하여 훈련 중 소수 클래스에 더 집중하게 만든다.

 

$$ FL(p_{t})=-(1-p_{t})^{\gamma}\log(p_{t}) $$

 

 여기서 pt 는 신경망에서 softmax activation을 통과 후 예측된 확률값이고, 조정가능한 γ≥0 focusing 파라미터이다. Focal Loss는 기존 교차 엔트로피 손실에 Modulating factor (1-pt)^γ 를 추가하여 교차 엔트로피에 대한 가중치를 조정한다.

 

 신경망이 잘못 분류하여 pt가 작으면, modulating factor 1에 가까워지고 Focal Loss는 영향을 받지 않는다. 그러나 신경망이 잘 분류하여 pt1에 가까워지면 modulating factor0에 가까워지고 잘 분류한 클래스에 대한 손실의 가중치는 줄어들게 된다. 한편, Focusing parameter 는 다수 클래스의 가중치를 줄이는 비율을 부드럽게 조정한다. γ=0이면 FL는 교차 엔트로피 손실과 동일하고, γ가 증가하면 modulating factor에 대한 영향도 마찬가지로 증가하게 된다. 결론적으로 moduling factor는 쉬운 샘플에 대한 손실 기여도를 줄이게 된다.

 

pytorch 이용한 Focal Loss 구현

import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    def __init__(self, gamma=0, alpha=None, size_average=True, device='cpu'):
        super(FocalLoss, self).__init__()
        """
        gamma(int) : focusing parameter.
        alpha(list) : alpha-balanced term.
        size_average(bool) : whether to apply reduction to the output.
        """
        self.gamma = gamma
        self.alpha = alpha
        self.size_average = size_average
        self.device = device

    def forward(self, input, target):
        # input : N * C (btach_size, num_class)
        # target : N (batch_size)

        CE = F.cross_entropy(input, target, reduction='none')  # -log(pt)
        pt = torch.exp(-CE)  # pt
        loss = (1 - pt) ** self.gamma * CE  # -(1-pt)^rlog(pt)

        if self.alpha is not None:
            alpha = torch.tensor(self.alpha, dtype=torch.float).to(self.device)
            # in case that a minority class is not selected when mini-batch sampling
            if len(self.alpha) != len(torch.unique(target)):
                temp = torch.zeros(len(self.alpha)).to(self.device)
                temp[torch.unique(target)] = alpha.index_select(0, torch.unique(target))
                alpha_t = temp.gather(0, target)
                loss = alpha_t * loss
            else:
                alpha_t = alpha.gather(0, target)
                loss = alpha_t * loss

        if self.size_average:
            loss = torch.mean(loss)

        return loss

gamma = 0이면 Focal Loss는 CrossEntropyLoss와 동일

 

gamma = 5일때 Focal Loss
alpha-balanced term을 적용한 Focal Loss

 

 

백혈병 데이터

  재발까지 걸린 시간 (단위: 주)
그룹1 6, 6, 6, 6+, 7, 9+, 10, 10+, 11+, 13
16, 17+, 19+, 20+, 22, 23, 25+, 25+, 32+, 32+, 34+
그룹2 1, 1, 2, 2, 3, 4, 4, 5, 5, 8,
8, 8, 8, 11, 11, 12, 12, 15, 17, 22, 23

중도절단 시점을 제외한 그룹1과 그룹2의 관측 시점은 다음과 같이 17개이다.

관측 시점 => 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 22, 23 (17개)

 

 

예) 7시점에서 관측 도수 분할표

  failed unfailed
그룹1 1 16 17
그룹2 0 12 12
1 28 29

 예를 들어, 7 시점에서 그룹1의 절단(failed) 사람의 수 $d_{17}$는 1명이고, 그룹2에서는 7시점에서 절단되지 않았으므로 $d_{27}$은 0이다. 또한, 그룹1의 7 시점에서 위험에 처한(at risk) 사람의 수 $Y_{17}$은 7시점을 포함하여 17명이고, 7 시점에서 그룹 2의 위험 사람의 수 $Y_{27}$는 12명이다.

위와 같은 분할표를 각 관측 시점에 대하여 만들면 다음과 같은 표를 작성하는 것이 편리하다.

 

 

  $d_{1i}$과 $d_{i2}$는 각각 관측시점 $i$에서 그룹1과 그룹2의 절단 수이고, $Y_{i1}$과 $Y_{i2}$는 각각 관측시점 $i$에서 그룹1과 그룹2의 위험(at risk) 수이다.

 

로그-순위 검정통계량이 $ \chi ^2 _{0.05}(1)=3.84 $보다 크므로 두 집단의 생존 곡선의 차이가 있다.

 

Python의 lifelines를 이용한 로그 순위검정

from lifelines.statistics import logrank_test
import numpy as np

time1 = np.array([6, 6, 6, 6, 7, 9, 10, 10, 11, 13, 16, 17, 19, 20, 22, 23, 25, 25, 32, 32, 34])
status1 = np.array([1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0])

time2 = np.array([1, 1, 2, 2, 3, 4, 4, 5, 5, 8, 8, 8, 8, 11, 11, 12, 12, 15, 17, 22, 23])
status2 = np.repeat(1, 21)

result = logrank_test(time1, time2,
                      status1, status2)
print(result.test_statistic)
print(result.p_value)

> 16.792940989216547

> 4.168809109334511e-05

 

from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt

kmf = KaplanMeierFitter(alpha = 0.05)
fig = plt.figure(figsize = (10, 10))

kmf.fit(time1, status1, label = 'Group 1')
kmf.plot()
kmf.fit(time2, status2, label = 'Group 2')
kmf.plot()
plt.xlabel('time')
plt.ylabel('surv prop')
plt.show()

 

 

R의 survival 패키지를 이용한 로그순위 검정

library(survival)
time1 <- c(6, 6, 6, 6, 7, 9, 10, 10, 11, 13, 
           16, 17, 19, 20, 22, 23, 25, 25, 32, 32, 34)
status1 <- c(1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 
             1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0)

time2 <- c(1, 1, 2, 2, 3, 4, 4, 5, 5, 8, 8, 8, 8, 
           11, 11, 12, 12, 15, 17, 22, 23)
status2 <- rep(1, 21)

time <- c(time1, time2)
status <- c(status1, status2)
group <- c(rep(1, 21), rep(2, 21)) # 1: trt, 2: placebo

fit = survdiff(Surv(time, status) ~ group)
fit

plot(survfit(Surv(time, status) ~ group), 
     lty=c(1,2), xlab = "time", ylab = "surv prob")
legend(20, 1, c('group1', 'group2'), lty = c(1,2))

 

 

 

'생존분석' 카테고리의 다른 글

[생존분석] 생존함수, 위험함수  (0) 2020.07.04

$i$번째 개체의 생존시간 $T_i$는 서로 독립이고, 중도절단시간 $C_i$도 서로 독립이라고 하자. 그러면 관측시간 $Y_i=min(T_i, C_i)$이고 각각의 관측값은 $(Y_i, \delta _i)$로 대응하게 된다. 여기서 $\delta _i=0$이면 중도절단된 경우이고 $\delta _i=1$이면 중도절단 되지 않은 경우다. 시점 t를 넘어 생존할 확률을 나타내는 생존함수(survival function) $S(t)$는

 

이고, 시점 t까지는 생존했다가 시점 t 바로 직후에 사망하게 되는 순간 위험률을 나타내는 위험함수 $h(t)$는

 

로 정의된다.

 

 T가 확률밀도함수 $f(x)$와 분포함수 $F(x)$를 갖는다고 하자. $S(t)$는 $S(0)=1, \lim _{t→∞}S(t)=0$이므로 생존함수 $S(t)$는

 

따라서 양변을 미분하면 $F(t)$는

 

 

또한, 생존함수 $f(x)$ 다음과 같이도 표현할 수 있다.

 

 

 


한편, 위험함수 $h(t)$는

 

$F(0)=1$을 이용하여 t에 대하여 적분하면 누적위험함수(culmulative harzard function) $H(t)$는

 

이므로 생존함수 $S(t)$는 누적위험함수 $H(t)$를 이용해 표현할 수 있다.

 

 

또한 위 식을 t에 대해 미분하면 생존함수 $f(t)$는 다음과 같이 표현할 수 있다.

 

 

 

 AdaBoost는 순차적으로 여러 개의 약분류기(weak classifier)를 결합시켜 하나의 강분류기를 구성하는 알고리즘으로 1996년 Freund와 Schapire가 제안한 부스팅 기법이다. 초기에는 모든 샘플에 대하여 동일한 가중치를 주고 약분류기를 학습시킨 뒤, 이전 분류기에서 잘못 분류된 샘플의 가중치를 크게함으로써 강분류기를 만들어가는 알고리즘이다.

 

 AdaBoost는 이진 분류(binary classification)에 최적화되어 있으며 학습자료가

이고, 반응변수 y가

인 경우라고 하면, AdaBoost 알고리즘은 약분류기 f(x)들을 결합시켜 강분류기 F(x)를 마드는 과정이다.

여기서 α는 약분류기의 가중치인 최적계수이다.

 

 

 

구체적인 학습과정은 초기 샘플들의 가중치를

로 초기화 하고 가중치가 적용된 오류율 e를 구한다.

error rate

약분류기의 최적계수 α를 계산한다.

weak classifier weighting coefficient

만약 무작위로 예측하여 오류율 e가 0.5에 가까워지면 (1-e)/e = 1이 되므로 약분류기의 가중치가 0에 가까워 지고, 오류율이 0.5보다 커지면 (1-e)/e < 1이 되어 약분류기의 가중치가 음수가  된다. 그 다음 모든 샘플의 부과하는 가중치를 

로 수정한다. 그런 다음 가중치들의 합이 1이 되도록 정규화 한다. 이 과정을 m = 1,...,M 번 반복하게 되면 약분류기 f에 의하여 잘못 분류된 샘플의 가중치를 높이는 방식으로 조정되게 된다.

 

 최종적으로 M단계 까지 적합된 강분류기 F(x)는

 

M개의 약분류기들의 선형결합으로 나타내어지며 다음 경계값 보다 큰 경우 1로 그렇지 않으면 -1로 분류하게 된다.

 

AdaBoost 적합 과정

 

'Machine Learning' 카테고리의 다른 글

지니지수(Gini index)를 이용한 최적 분류점 찾기  (0) 2020.06.28
Random Forest  (0) 2020.06.28

+ Recent posts